From 55ede8021a76ab6dcc135594b0c07cd72960eff6 Mon Sep 17 00:00:00 2001 From: shangxinli Date: Thu, 8 Oct 2026 21:09:17 +0900 Subject: [PATCH 1/3] feat: add EncryptionManager and StandardEncryptionManager - EncryptionManager (core): per-table interface to generate key metadata, open AES GCM Stream files for reading and writing, and encrypt and decrypt manifest list key metadata. - EncryptionManagerRegistry (core): creates the manager of an encrypted table from the trusted table key ID, the table's encryption-keys and the catalog's KMS client. The bundle registers the implementation, so the core library has no crypto dependency. - StandardEncryptionManager (bundle): Java's envelope encryption. Key encryption keys are wrapped by the table master key in the KMS, timestamped, used for 730 days and then rotated; manifest list key metadata is encrypted by a KEK with the KEK timestamp as AAD. Unwrapped KEKs are cached for one hour. Thread-safe. - encryption::RegisterAll() / IsEncryptionAvailable() in the bundle; a no-op when built with ICEBERG_ENCRYPTION=OFF. Adds the Java-written encrypted table fixture under test/resources/encryption/table. Tests decrypt the manifest list key metadata of every Java snapshot and check that its file length is the stored size of the manifest list. --- src/iceberg/CMakeLists.txt | 10 +- src/iceberg/encryption/encryption_manager.cc | 72 +++++ src/iceberg/encryption/encryption_manager.h | 120 ++++++++ src/iceberg/encryption/encryption_register.cc | 36 +++ src/iceberg/encryption/encryption_register.h | 36 +++ .../encryption/standard_encryption_manager.cc | 268 ++++++++++++++++++ .../encryption/standard_encryption_manager.h | 113 ++++++++ src/iceberg/test/CMakeLists.txt | 6 +- ...b-c6f6-4ce2-a242-a0745cf50318-00001.puffin | Bin 0 -> 463 bytes .../encryption/table/data/data-0.parquet | Bin 0 -> 1791 bytes .../encryption/table/data/data-1.parquet | Bin 0 -> 1793 bytes .../encryption/table/data/data-2.avro | Bin 0 -> 445 bytes ...2964d7-9cf9-4547-a01b-29e4eef4bc25-m0.avro | Bin 0 -> 7889 bytes ...1c33d0-d9b9-4c80-bce0-b6fe4679785b-m0.avro | Bin 0 -> 7886 bytes ...f19655-198b-4c2a-a7e5-e3cf75d5ab54-m0.avro | Bin 0 -> 7844 bytes ...977369-b04a-4cf3-a491-6090bc1572fc-m0.avro | Bin 0 -> 7909 bytes ...-462964d7-9cf9-4547-a01b-29e4eef4bc25.avro | Bin 0 -> 4940 bytes ...-95f19655-198b-4c2a-a7e5-e3cf75d5ab54.avro | Bin 0 -> 5045 bytes ...-d1977369-b04a-4cf3-a491-6090bc1572fc.avro | Bin 0 -> 5145 bytes ...-511c33d0-d9b9-4c80-bce0-b6fe4679785b.avro | Bin 0 -> 4825 bytes .../table/metadata/v1.metadata.json | 1 + .../table/metadata/v2.metadata.json | 1 + .../table/metadata/v3.metadata.json | 1 + .../table/metadata/v4.metadata.json | 1 + .../table/metadata/v5.metadata.json | 1 + .../table/metadata/version-hint.text | 1 + .../test/standard_encryption_manager_test.cc | 224 +++++++++++++++ 27 files changed, 889 insertions(+), 2 deletions(-) create mode 100644 src/iceberg/encryption/encryption_manager.cc create mode 100644 src/iceberg/encryption/encryption_manager.h create mode 100644 src/iceberg/encryption/encryption_register.cc create mode 100644 src/iceberg/encryption/encryption_register.h create mode 100644 src/iceberg/encryption/standard_encryption_manager.cc create mode 100644 src/iceberg/encryption/standard_encryption_manager.h create mode 100644 src/iceberg/test/resources/encryption/table/data/00001-1-86fba0db-c6f6-4ce2-a242-a0745cf50318-00001.puffin create mode 100644 src/iceberg/test/resources/encryption/table/data/data-0.parquet create mode 100644 src/iceberg/test/resources/encryption/table/data/data-1.parquet create mode 100644 src/iceberg/test/resources/encryption/table/data/data-2.avro create mode 100644 src/iceberg/test/resources/encryption/table/metadata/462964d7-9cf9-4547-a01b-29e4eef4bc25-m0.avro create mode 100644 src/iceberg/test/resources/encryption/table/metadata/511c33d0-d9b9-4c80-bce0-b6fe4679785b-m0.avro create mode 100644 src/iceberg/test/resources/encryption/table/metadata/95f19655-198b-4c2a-a7e5-e3cf75d5ab54-m0.avro create mode 100644 src/iceberg/test/resources/encryption/table/metadata/d1977369-b04a-4cf3-a491-6090bc1572fc-m0.avro create mode 100644 src/iceberg/test/resources/encryption/table/metadata/snap-1122461367364127429-1-462964d7-9cf9-4547-a01b-29e4eef4bc25.avro create mode 100644 src/iceberg/test/resources/encryption/table/metadata/snap-5112456484007752895-1-95f19655-198b-4c2a-a7e5-e3cf75d5ab54.avro create mode 100644 src/iceberg/test/resources/encryption/table/metadata/snap-5555854819025587960-1-d1977369-b04a-4cf3-a491-6090bc1572fc.avro create mode 100644 src/iceberg/test/resources/encryption/table/metadata/snap-564307663132197943-1-511c33d0-d9b9-4c80-bce0-b6fe4679785b.avro create mode 100644 src/iceberg/test/resources/encryption/table/metadata/v1.metadata.json create mode 100644 src/iceberg/test/resources/encryption/table/metadata/v2.metadata.json create mode 100644 src/iceberg/test/resources/encryption/table/metadata/v3.metadata.json create mode 100644 src/iceberg/test/resources/encryption/table/metadata/v4.metadata.json create mode 100644 src/iceberg/test/resources/encryption/table/metadata/v5.metadata.json create mode 100644 src/iceberg/test/resources/encryption/table/metadata/version-hint.text create mode 100644 src/iceberg/test/standard_encryption_manager_test.cc diff --git a/src/iceberg/CMakeLists.txt b/src/iceberg/CMakeLists.txt index cf465e5fdc..b116fe85de 100644 --- a/src/iceberg/CMakeLists.txt +++ b/src/iceberg/CMakeLists.txt @@ -33,6 +33,7 @@ set(ICEBERG_SOURCES deletes/position_delete_index.cc deletes/position_delete_range_consumer.cc deletes/roaring_position_bitmap.cc + encryption/encryption_manager.cc encryption/key_management_client.cc encryption/standard_key_metadata.cc expression/aggregate.cc @@ -321,6 +322,7 @@ if(ICEBERG_BUILD_BUNDLE) avro/avro_register.cc avro/avro_schema_util.cc avro/avro_stream_internal.cc + encryption/encryption_register.cc parquet/parquet_data_util.cc parquet/parquet_metrics.cc parquet/parquet_metrics_row_group_filter.cc @@ -335,7 +337,8 @@ if(ICEBERG_BUILD_BUNDLE) ICEBERG_BUNDLE_SOURCES encryption/aes_gcm.cc encryption/aes_gcm_stream.cc - encryption/in_memory_kms.cc) + encryption/in_memory_kms.cc + encryption/standard_encryption_manager.cc) endif() # Libraries to link with exported libiceberg_bundle.{so,a}. @@ -410,6 +413,11 @@ if(ICEBERG_BUILD_BUNDLE) foreach(target iceberg_bundle_static iceberg_bundle_shared) if(TARGET ${target}) + if(ICEBERG_ENCRYPTION) + target_compile_definitions(${target} PRIVATE ICEBERG_ENCRYPTION_ENABLED=1) + else() + target_compile_definitions(${target} PRIVATE ICEBERG_ENCRYPTION_ENABLED=0) + endif() if(ICEBERG_S3) target_compile_definitions(${target} PUBLIC "$") diff --git a/src/iceberg/encryption/encryption_manager.cc b/src/iceberg/encryption/encryption_manager.cc new file mode 100644 index 0000000000..a49d86d1fc --- /dev/null +++ b/src/iceberg/encryption/encryption_manager.cc @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "iceberg/encryption/encryption_manager.h" + +#include + +#include "iceberg/util/macros.h" + +namespace iceberg { + +namespace { + +struct RegistryState { + std::mutex mutex; + EncryptionManagerRegistry::Factory factory; +}; + +RegistryState& State() { + static RegistryState state; + return state; +} + +} // namespace + +void EncryptionManagerRegistry::Register(Factory factory) { + auto& state = State(); + std::lock_guard lock(state.mutex); + state.factory = std::move(factory); +} + +Result> EncryptionManagerRegistry::Make( + Options options) { + ICEBERG_PRECHECK(!options.table_key_id.empty(), "Invalid encryption key ID: empty"); + ICEBERG_PRECHECK(options.data_key_length == 16 || options.data_key_length == 24 || + options.data_key_length == 32, + "Invalid data key length: {} (must be 16, 24, or 32)", + options.data_key_length); + ICEBERG_PRECHECK(options.kms != nullptr, + "Cannot create an encryption manager without a KMS client; set the " + "encryption.kms-type or encryption.kms-impl catalog property"); + Factory factory; + { + auto& state = State(); + std::lock_guard lock(state.mutex); + factory = state.factory; + } + if (!factory) { + return NotSupported( + "Table encryption is not available: link iceberg_bundle built with " + "ICEBERG_ENCRYPTION=ON and call iceberg::encryption::RegisterAll()"); + } + return factory(std::move(options)); +} + +} // namespace iceberg diff --git a/src/iceberg/encryption/encryption_manager.h b/src/iceberg/encryption/encryption_manager.h new file mode 100644 index 0000000000..83ccaa99d3 --- /dev/null +++ b/src/iceberg/encryption/encryption_manager.h @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +/// \file iceberg/encryption/encryption_manager.h +/// Table encryption manager interface and the registry that creates one. + +#include +#include +#include +#include +#include +#include +#include + +#include "iceberg/encryption/encrypted_key.h" +#include "iceberg/encryption/key_management_client.h" +#include "iceberg/encryption/standard_key_metadata.h" +#include "iceberg/file_io.h" +#include "iceberg/iceberg_export.h" +#include "iceberg/result.h" + +namespace iceberg { + +/// \brief Keys created when the key metadata of a new manifest list is registered. +/// +/// Both must be added to the table metadata `encryption-keys` in the same commit as +/// the snapshot whose `key-id` is `file_key.key_id`. +struct ICEBERG_EXPORT ManifestListEncryptionKeys { + /// The key encryption key, wrapped by the table master key in the KMS. + EncryptedKey key_encryption_key; + /// The manifest list key metadata, encrypted by the key encryption key. + EncryptedKey file_key; +}; + +/// \brief Encrypts and decrypts the files of one table. +/// +/// The standard implementation (StandardEncryptionManager) needs a crypto provider +/// and lives in the bundle library; the core library reaches it through +/// EncryptionManagerRegistry. +class ICEBERG_EXPORT EncryptionManager { + public: + virtual ~EncryptionManager() = default; + + /// \brief Generate key metadata (random data key and AAD prefix) for a new file. + virtual Result NewKeyMetadata() = 0; + + /// \brief Wrap an AES GCM Stream (AGS1) file in a decrypting InputFile. + /// + /// \param encrypted the encrypted file + /// \param key_metadata serialized key metadata, including the file length + virtual Result> NewDecryptingInputFile( + std::unique_ptr encrypted, std::span key_metadata) = 0; + + /// \brief Wrap a file in an AES GCM Stream (AGS1) encrypting OutputFile. + virtual Result> NewEncryptingOutputFile( + std::unique_ptr plain, const StandardKeyMetadata& key_metadata) = 0; + + /// \brief Decrypt the key metadata of a manifest list. + /// + /// \param manifest_list_key_id the snapshot `key-id` + /// \return the serialized StandardKeyMetadata of the manifest list file + virtual Result> DecryptManifestListKeyMetadata( + std::string_view manifest_list_key_id) = 0; + + /// \brief Encrypt the key metadata of a new manifest list with a key encryption key. + /// + /// Creates a key encryption key (wrapped by the table master key) if there is no + /// unexpired one. + virtual Result RegisterManifestListKeyMetadata( + const StandardKeyMetadata& key_metadata) = 0; +}; + +/// \brief Creates the encryption manager of encrypted tables. +/// +/// The bundle library registers the standard implementation (see +/// iceberg::encryption::RegisterAll()). Without it, encrypted tables cannot be read or +/// written. +class ICEBERG_EXPORT EncryptionManagerRegistry { + public: + struct Options { + /// The table master key ID in the KMS (table property `encryption.key-id`), taken + /// from a trusted source. + std::string table_key_id; + /// Length of generated data keys: 16, 24 or 32 bytes. + int32_t data_key_length = 16; + /// The table metadata `encryption-keys`. + std::vector encryption_keys; + /// The catalog's KMS client. + std::shared_ptr kms; + }; + + using Factory = + std::function>(Options options)>; + + /// \brief Register the encryption manager implementation. + static void Register(Factory factory); + + /// \brief Create an encryption manager. + static Result> Make(Options options); +}; + +} // namespace iceberg diff --git a/src/iceberg/encryption/encryption_register.cc b/src/iceberg/encryption/encryption_register.cc new file mode 100644 index 0000000000..3df7383c2b --- /dev/null +++ b/src/iceberg/encryption/encryption_register.cc @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "iceberg/encryption/encryption_register.h" + +#if ICEBERG_ENCRYPTION_ENABLED +# include "iceberg/encryption/standard_encryption_manager.h" +#endif + +namespace iceberg::encryption { + +void RegisterAll() { +#if ICEBERG_ENCRYPTION_ENABLED + RegisterStandardEncryptionManager(); +#endif +} + +bool IsEncryptionAvailable() { return ICEBERG_ENCRYPTION_ENABLED; } + +} // namespace iceberg::encryption diff --git a/src/iceberg/encryption/encryption_register.h b/src/iceberg/encryption/encryption_register.h new file mode 100644 index 0000000000..beacad013e --- /dev/null +++ b/src/iceberg/encryption/encryption_register.h @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +/// \file iceberg/encryption/encryption_register.h +/// Registration of the table encryption implementation of the bundle library. + +#include "iceberg/iceberg_bundle_export.h" + +namespace iceberg::encryption { + +/// \brief Register the standard encryption manager, if the bundle was built with +/// ICEBERG_ENCRYPTION=ON. Otherwise a no-op, and encrypted tables are not supported. +ICEBERG_BUNDLE_EXPORT void RegisterAll(); + +/// \brief Whether the bundle was built with table encryption support. +ICEBERG_BUNDLE_EXPORT bool IsEncryptionAvailable(); + +} // namespace iceberg::encryption diff --git a/src/iceberg/encryption/standard_encryption_manager.cc b/src/iceberg/encryption/standard_encryption_manager.cc new file mode 100644 index 0000000000..40e3fb673a --- /dev/null +++ b/src/iceberg/encryption/standard_encryption_manager.cc @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "iceberg/encryption/standard_encryption_manager.h" + +#include +#include +#include + +#include "iceberg/encryption/aes_gcm_internal.h" +#include "iceberg/encryption/aes_gcm_stream.h" +#include "iceberg/util/base64.h" +#include "iceberg/util/macros.h" + +namespace iceberg { + +namespace { + +using encryption::AesGcmCipher; +using encryption::SecureRandomBytes; + +std::span AsBytes(std::string_view s) { + return {reinterpret_cast(s.data()), s.size()}; +} + +std::string ToString(std::span bytes) { + return {reinterpret_cast(bytes.data()), bytes.size()}; +} + +Result GenerateKeyId() { + std::array id{}; + ICEBERG_RETURN_UNEXPECTED(SecureRandomBytes(id)); + return Base64::Encode(ToString(id)); +} + +std::optional ParseTimestamp(const EncryptedKey& key) { + auto it = key.properties.find(std::string(StandardEncryptionManager::kKeyTimestamp)); + if (it == key.properties.end()) { + return std::nullopt; + } + int64_t value = 0; + const auto& str = it->second; + auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), value); + if (ec != std::errc{} || ptr != str.data() + str.size()) { + return std::nullopt; + } + return value; +} + +} // namespace + +Result> StandardEncryptionManager::Make( + EncryptionManagerRegistry::Options options) { + ICEBERG_PRECHECK(!options.table_key_id.empty(), "Invalid encryption key ID: empty"); + ICEBERG_PRECHECK(options.data_key_length == 16 || options.data_key_length == 24 || + options.data_key_length == 32, + "Invalid data key length: {} (must be 16, 24, or 32)", + options.data_key_length); + ICEBERG_PRECHECK(options.kms != nullptr, "Invalid KMS client: null"); + return std::shared_ptr( + new StandardEncryptionManager(std::move(options))); +} + +StandardEncryptionManager::StandardEncryptionManager( + EncryptionManagerRegistry::Options options) + : table_key_id_(std::move(options.table_key_id)), + data_key_length_(options.data_key_length), + kms_(std::move(options.kms)) { + for (auto& key : options.encryption_keys) { + AddKey(std::move(key)); + } +} + +void StandardEncryptionManager::AddKey(EncryptedKey key) { + auto key_id = key.key_id; + if (keys_.insert_or_assign(key_id, std::move(key)).second) { + key_order_.push_back(std::move(key_id)); + } +} + +void StandardEncryptionManager::SetTestTimeShift(std::chrono::milliseconds shift) { + std::lock_guard lock(mutex_); + time_shift_ = shift; +} + +int64_t StandardEncryptionManager::NowMs() const { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + time_shift_) + .count(); +} + +Result StandardEncryptionManager::NewKeyMetadata() { + StandardKeyMetadata metadata; + metadata.encryption_key.resize(data_key_length_); + ICEBERG_RETURN_UNEXPECTED(SecureRandomBytes(metadata.encryption_key)); + metadata.aad_prefix.emplace(kAadPrefixLength); + ICEBERG_RETURN_UNEXPECTED(SecureRandomBytes(*metadata.aad_prefix)); + return metadata; +} + +Result> StandardEncryptionManager::NewDecryptingInputFile( + std::unique_ptr encrypted, std::span key_metadata) { + ICEBERG_PRECHECK(encrypted != nullptr, "Encrypted input file cannot be null"); + ICEBERG_ASSIGN_OR_RAISE(auto metadata, StandardKeyMetadata::Parse(key_metadata)); + if (!metadata.file_length.has_value()) { + // The length must come from trusted key metadata, not from the file system, so that + // truncation is detected (format/gcm-stream-spec.md, "File length"). + return Invalid("Cannot read AES GCM stream {}: key metadata has no file length", + encrypted->location()); + } + return std::make_unique( + std::move(encrypted), std::move(metadata.encryption_key), + metadata.aad_prefix.value_or(std::vector{}), *metadata.file_length); +} + +Result> StandardEncryptionManager::NewEncryptingOutputFile( + std::unique_ptr plain, const StandardKeyMetadata& key_metadata) { + ICEBERG_PRECHECK(plain != nullptr, "Output file cannot be null"); + return std::make_unique( + std::move(plain), key_metadata.encryption_key, + key_metadata.aad_prefix.value_or(std::vector{})); +} + +Result> StandardEncryptionManager::UnwrapKek( + const std::string& kek_id) { + int64_t now = NowMs(); + if (auto it = unwrapped_keks_.find(kek_id); it != unwrapped_keks_.end()) { + if (now - it->second.unwrapped_at_ms < kUnwrappedKekCacheExpiry.count()) { + return it->second.key; + } + unwrapped_keks_.erase(it); + } + auto it = keys_.find(kek_id); + if (it == keys_.end()) { + return Invalid("Cannot find key encryption key with id {}", kek_id); + } + ICEBERG_ASSIGN_OR_RAISE( + auto kek, + kms_->UnwrapKey(AsBytes(it->second.encrypted_key_metadata), table_key_id_)); + unwrapped_keks_.insert_or_assign(kek_id, CachedKek{kek, now}); + return kek; +} + +Result> StandardEncryptionManager::DecryptManifestListKeyMetadata( + std::string_view manifest_list_key_id) { + std::lock_guard lock(mutex_); + auto it = keys_.find(std::string(manifest_list_key_id)); + if (it == keys_.end()) { + return Invalid("Cannot find manifest list key metadata with id {}", + manifest_list_key_id); + } + const EncryptedKey& manifest_list_key = it->second; + if (!manifest_list_key.encrypted_by_id.has_value() || + *manifest_list_key.encrypted_by_id == table_key_id_) { + return Invalid("{} is not manifest list key metadata", manifest_list_key_id); + } + + const std::string& kek_id = *manifest_list_key.encrypted_by_id; + ICEBERG_ASSIGN_OR_RAISE(auto kek, UnwrapKek(kek_id)); + auto timestamp = keys_.at(kek_id).properties.find(std::string(kKeyTimestamp)); + if (timestamp == keys_.at(kek_id).properties.end()) { + return Invalid("Key encryption key {} must be timestamped", kek_id); + } + + // The KEK timestamp is the AAD, to prevent timestamp tampering attacks. + ICEBERG_ASSIGN_OR_RAISE(auto cipher, AesGcmCipher::Make(kek)); + return cipher->Decrypt(AsBytes(manifest_list_key.encrypted_key_metadata), + AsBytes(timestamp->second)); +} + +Result StandardEncryptionManager::CurrentKekId() { + int64_t now = NowMs(); + for (const auto& key_id : key_order_) { + const EncryptedKey& key = keys_.at(key_id); + if (key.encrypted_by_id != table_key_id_) { + continue; + } + auto timestamp = ParseTimestamp(key); + if (timestamp.has_value() && now - *timestamp < kKekLifespan.count()) { + return key_id; + } + } + + // No unexpired key encryption key: create one, wrapped by the table master key. + std::vector kek(data_key_length_); + ICEBERG_RETURN_UNEXPECTED(SecureRandomBytes(kek)); + ICEBERG_ASSIGN_OR_RAISE(auto wrapped, kms_->WrapKey(kek, table_key_id_)); + ICEBERG_ASSIGN_OR_RAISE(auto kek_id, GenerateKeyId()); + AddKey(EncryptedKey{.key_id = kek_id, + .encrypted_key_metadata = ToString(wrapped), + .encrypted_by_id = table_key_id_, + .properties = {{std::string(kKeyTimestamp), std::to_string(now)}}}); + unwrapped_keks_.insert_or_assign(kek_id, CachedKek{std::move(kek), now}); + return kek_id; +} + +Result +StandardEncryptionManager::RegisterManifestListKeyMetadata( + const StandardKeyMetadata& key_metadata) { + ICEBERG_PRECHECK(key_metadata.file_length.has_value(), + "Manifest list key metadata must include the file length"); + std::lock_guard lock(mutex_); + ICEBERG_ASSIGN_OR_RAISE(auto kek_id, CurrentKekId()); + ICEBERG_ASSIGN_OR_RAISE(auto kek, UnwrapKek(kek_id)); + const EncryptedKey& kek_entry = keys_.at(kek_id); + const std::string& timestamp = kek_entry.properties.at(std::string(kKeyTimestamp)); + + ICEBERG_ASSIGN_OR_RAISE(auto cipher, AesGcmCipher::Make(kek)); + ICEBERG_ASSIGN_OR_RAISE(auto encrypted, + cipher->Encrypt(key_metadata.Serialize(), AsBytes(timestamp))); + ICEBERG_ASSIGN_OR_RAISE(auto file_key_id, GenerateKeyId()); + EncryptedKey file_key{.key_id = file_key_id, + .encrypted_key_metadata = ToString(encrypted), + .encrypted_by_id = kek_id}; + AddKey(file_key); + return ManifestListEncryptionKeys{.key_encryption_key = keys_.at(kek_id), + .file_key = std::move(file_key)}; +} + +std::vector StandardEncryptionManager::encryption_keys() const { + std::lock_guard lock(mutex_); + std::vector result; + result.reserve(key_order_.size()); + for (const auto& key_id : key_order_) { + result.push_back(keys_.at(key_id)); + } + return result; +} + +namespace encryption { + +void RegisterStandardEncryptionManager() { + EncryptionManagerRegistry::Register([](EncryptionManagerRegistry::Options options) + -> Result> { + ICEBERG_ASSIGN_OR_RAISE(auto manager, + StandardEncryptionManager::Make(std::move(options))); + return manager; + }); +} + +namespace { + +[[maybe_unused]] const bool kStandardEncryptionManagerRegistered = []() { + RegisterStandardEncryptionManager(); + return true; +}(); + +} // namespace + +} // namespace encryption + +} // namespace iceberg diff --git a/src/iceberg/encryption/standard_encryption_manager.h b/src/iceberg/encryption/standard_encryption_manager.h new file mode 100644 index 0000000000..19a242dec1 --- /dev/null +++ b/src/iceberg/encryption/standard_encryption_manager.h @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +/// \file iceberg/encryption/standard_encryption_manager.h +/// Envelope encryption manager, compatible with Java's StandardEncryptionManager. + +#include +#include +#include +#include +#include +#include + +#include "iceberg/encryption/encryption_manager.h" +#include "iceberg/iceberg_bundle_export.h" + +namespace iceberg { + +/// \brief Standard Iceberg envelope encryption. +/// +/// Key hierarchy: +/// - table master key: in the KMS, ID in table property `encryption.key-id`; +/// - key encryption keys (KEK): wrapped by the KMS and kept in `encryption-keys` +/// with `encrypted-by-id` = the table key ID and a KEY_TIMESTAMP property. A KEK is +/// used for 730 days (NIST SP 800-57), then a new one is created; +/// - manifest list key metadata: AES-GCM encrypted by a KEK with the KEK timestamp as +/// AAD, kept in `encryption-keys` and referenced by the snapshot `key-id`; +/// - manifest key metadata: in the manifest list `key_metadata` field; +/// - data and delete file key metadata: in the manifest `key_metadata` field. +/// +/// Unwrapped KEKs are cached for one hour. Thread-safe. +class ICEBERG_BUNDLE_EXPORT StandardEncryptionManager : public EncryptionManager { + public: + static constexpr std::string_view kKeyTimestamp = "KEY_TIMESTAMP"; + static constexpr int32_t kAadPrefixLength = 16; + static constexpr std::chrono::milliseconds kKekLifespan{730LL * 24 * 3600 * 1000}; + static constexpr std::chrono::milliseconds kUnwrappedKekCacheExpiry{3600LL * 1000}; + + static Result> Make( + EncryptionManagerRegistry::Options options); + + Result NewKeyMetadata() override; + Result> NewDecryptingInputFile( + std::unique_ptr encrypted, + std::span key_metadata) override; + Result> NewEncryptingOutputFile( + std::unique_ptr plain, + const StandardKeyMetadata& key_metadata) override; + Result> DecryptManifestListKeyMetadata( + std::string_view manifest_list_key_id) override; + Result RegisterManifestListKeyMetadata( + const StandardKeyMetadata& key_metadata) override; + + /// \brief All keys: those from table metadata and those created by this manager. + std::vector encryption_keys() const; + + const std::string& table_key_id() const { return table_key_id_; } + + /// \brief Shift the clock, for tests of KEK rotation and cache expiry. + void SetTestTimeShift(std::chrono::milliseconds shift); + + private: + struct CachedKek { + std::vector key; + int64_t unwrapped_at_ms; + }; + + explicit StandardEncryptionManager(EncryptionManagerRegistry::Options options); + + // The following require mutex_ to be held. + Result> UnwrapKek(const std::string& kek_id); + Result CurrentKekId(); + void AddKey(EncryptedKey key); + + int64_t NowMs() const; + + const std::string table_key_id_; + const int32_t data_key_length_; + const std::shared_ptr kms_; + + mutable std::mutex mutex_; + std::chrono::milliseconds time_shift_{0}; + std::vector key_order_; + std::unordered_map keys_; + std::unordered_map unwrapped_keks_; +}; + +namespace encryption { + +/// \brief Register StandardEncryptionManager in EncryptionManagerRegistry. +ICEBERG_BUNDLE_EXPORT void RegisterStandardEncryptionManager(); + +} // namespace encryption + +} // namespace iceberg diff --git a/src/iceberg/test/CMakeLists.txt b/src/iceberg/test/CMakeLists.txt index 2513ff177e..919e6dd504 100644 --- a/src/iceberg/test/CMakeLists.txt +++ b/src/iceberg/test/CMakeLists.txt @@ -243,7 +243,11 @@ if(ICEBERG_BUILD_BUNDLE) endif() if(ICEBERG_ENCRYPTION) - add_iceberg_test(crypto_test USE_BUNDLE SOURCES aes_gcm_test.cc) + add_iceberg_test(crypto_test + USE_BUNDLE + SOURCES + aes_gcm_test.cc + standard_encryption_manager_test.cc) endif() add_iceberg_test(scan_test diff --git a/src/iceberg/test/resources/encryption/table/data/00001-1-86fba0db-c6f6-4ce2-a242-a0745cf50318-00001.puffin b/src/iceberg/test/resources/encryption/table/data/00001-1-86fba0db-c6f6-4ce2-a242-a0745cf50318-00001.puffin new file mode 100644 index 0000000000000000000000000000000000000000..9f6665dbf6080d289bb5ac84419b8f17322de177 GIT binary patch literal 463 zcmV;=0WkhSM^iBX01yDI1CcWPktfIHesiQ*`AgAe-Twzi{~KPyO?3H%+-kM>4Y`g+KE-B7FtRvqLIDPQXO5ac!u<1^N2p;k};eWo(GuXBPK zoa%fLKtZt9&ipKrbvh-i>ezOqpm7+QY{jeBp&E3!$vfM_kx+XhQHoC=_%9Iun(uR& zE3ZcPnA=)8HjcH;%c1AJ#+Y5;(x(^6BM4(;ZI<3o4tuJ2g^=8^rW57vyGPRXkTh2{ zu;EGHO^5|6pJfXY=<{>rD8j>eG>T$YuGaoX9!=JLlA7hxlf$)2|Hsx4co)q|db+O# z`scIah&7J}Gp4^wX-2ulNn{t*sBEO-D4?Em5|-M2yljz%xoPUAL_ zerx4CVMyibaqc!9v(WTn8PNd!KbxX)g1O8~{e)ZdPUDV4&oh8@)}~q|hJ!+ZDrJIx zNq_(V02nc>m3WA+t`I#uGXAO|EyihiknM#a2u9PaarM|O;3aAM@JTPD8B9zjZ?goX z>_6+;V3PA8vyR!?8(bJLe+c>X*y()QZvJ) zST4W~c6FlZ+(vR45=oda+cbx8rO{UMv`vFOghc=V0AjN`;S6H0LQz+DY$!)o^&2|N zOU&XLdB~Q~A`5;)!iB?sLkH;#Y@Ao!pIIlsJCBC_S2YxuwpU^!Tud2DB*AVc%GyCZB-)WD!U% zyo{y~0@D6|;Lq!q?<-cV-@xwGsk)52Hzuey{L2qS3h0$1h4wTFnB~&e_cqEZ}{$iTu3|; z?1wKp1h^;Ju*Tcsv$#9y6IKT4+t;CsCdTagXlZ_R+d?fW8!@&ep#)9lI5mz{w^}eP z9^QZe007)%KTk`d=04Va-9dz;N!{N>Ml_|S+l0kINePqM6#lpGmmg1~2$_7B)EQC1 z@|ePDvx{J`?x$R80}k;u@nN-MR)_sHyh^2z_&zHlz%~h)U9P%`c|h-i-_GN77H*7p zYCD;e7U+6Vt?nn5z}(VYh_r5Fbqb%-7Q!xKdGmk(006(#Ht+uIk;_W1kWnuHd#zP1 zzk0c`rXh^SSj}HeQ;b_}T>$QOy!v_g8){}UAi}b&Tb&AO{GXh5YeN}L$xu_#1;$rX z{d}Akgau2pIG|IA7nIv%`&Kl5#Jd3&WBmSii2oA$ZCObj^;{r67#BX2ws?;sGEgy0 z{ke#eO?yMZAbkAV7=>h)%@`-l# z?r@0UyRDAB%3&_64f45-GyQ@;p+%{aH?~@l*fO0V?NIqE|LNea z1tt5GU-iRa*q|GK1Qb~6C1QYyZ=+xM!7ArMFIwZ={4ACcrU5nwPkMJX^VW9bjrZ-J zEoy7uQ3@@u=?d6WTHTW^`J)?FgtI_^U{i>OWfPP&upyLkuI|R*QM0}?Gr?&GJ@Mqb zZAF(Rv%|Oc|CnYu|CCQdRFmkP##jkryT0$rr1aBzSmB?`#S^PF_$@0o_F^n44`DxR z_>cT=td@WwO6DgeSo}2KCrr;vHx1iSLFqaZJKijRcl-a!;<3TJSJB$;yh-vzZsfE@ zhgKs(GEW&po6m&hB1X2>SG7DQGu+t8)!JMu&+Vlg$q7CUon)ZVDque{d&;ROV2$r z@hxZWTE&2?l7TvCFq*ZOushV9?h`YbGOkVqBt&L>1>=@@F3JP1rtEd+3gD8N^|uv^ zcFxM1z@X9msN{1D`f-x`)GJ2jXySa&u(}=Bh1p=3Jk>6uME{qVUhz^_jI5NjTTWon z^vj2YEXa`~8@ixO%20wROlJNNdbb-#feV7sOz_(h$BQ5KidI3 zjuHE5TzN3#l`#?%tZ9C=J{{G8OPgBZ$+lGiD(}J;pWhg#hBO@{gMh}ipj@rd=-DS} h(m#z35LpGotqtY06Xk}KkH8glrT0(+002-yQbqK`evkkF literal 0 HcmV?d00001 diff --git a/src/iceberg/test/resources/encryption/table/data/data-1.parquet b/src/iceberg/test/resources/encryption/table/data/data-1.parquet new file mode 100644 index 0000000000000000000000000000000000000000..e94835ef5559c071a8e10d9f023d8fe469fb5011 GIT binary patch literal 1793 zcmV+c2mbg_K~hD400007x=cddBLG!>u0aLprMTc!$xo1Hy(CqrRIFV%YhI%V_j0NV z*re7_K@@3!HxM}yauOKK@In${U+L0002T{+D}X%fm)z zkODYP5WX~z(>M;wuW`}_s=Dx~*7)I53vjwxX-KYbnk8jX+J8fa>pGxvlJxTLxhE|< z9)JJ<0RJSs?D4L+LVuiEHqAmbrem`GwKYWBl>UTUr+I_b~+GGpJ{F7=?_ zkwPO3U2XvlMFxC@zZ0aFS-gCZ{M;7Cgh}Z}$VUJG0K4}+ekLXUUrB>Ols zfB*mh81hXt*QMiS)C-F=LSNx%q0c%1mMChy#cb;kUaXHTbia6Pn7&pM)Q&EOYS09l!Pzj2FdHeFRv~Re9g8c)OGPvlMh{}6si-r6 z(XCGQ#-Y5+4LGhtbPmd>+G=cj;Kab z-MBS+xeVqR^1{1w)e`EwFzEZbUl=iu-6Qf10r4yVlz|O#*;f}ji$f9k^@3je3{|Ag zmb%M1&0><%^s}J9?`du-%oqUHnXa!&a+|53>L`14JX!5B;CH?jNMc>%s!Y5XB5UvIbw1 z{>i=__gsKLW~Ps{Y>CaI02$g1VI<*$gee~m;G|=urJvKT94HgERaFMEWLF_=34?%K zi}k|Cra}fmG>DxY#&}iIWB?QW$=9HFl`&(21c;1K04+FZbwZuKd(oB>ZwuYr?zoF4 zadPOU-4sh_rGk`z++%?K%XH}A*IHoUs=S#E4r@r3?&Tk}3pqXDT5ex?kDU9D z=eW?;oW;I1oqk86=7uOT!4PmiyM>e$=G|mpeOvfMwp7T!0(ZiPYljZW z#-1%LapLh%LJUk!1rmfsuTCV!BZ+wRFh6fU#EJ3`?q~-XB(|hRF`!5$wdz#}%du3D? znPb5$Wm6xV_eSzigjDj*u@=B?`2#XOmYGvWP-6S-*&>z3(sIQV6ynflCv=)4%PXc{ z)@cw9W!vtoDN`UPJMkADgl=OYom6Nva}rfSCgc7Ehk!HsbUQU)>Rz2&UW#L0lC+_T zjrVRaR2d;3hOUp#MHfLrf%$%%D5e%gM4~t9b-6~DgVoP?(GY{6K{v&P$L;8HTJ6m& jRpM5mWE7y*>Xfbi(4(}fSG?yh_s)c9Py+w}P(e~f0##m@ literal 0 HcmV?d00001 diff --git a/src/iceberg/test/resources/encryption/table/data/data-2.avro b/src/iceberg/test/resources/encryption/table/data/data-2.avro new file mode 100644 index 0000000000000000000000000000000000000000..554247456662cd3dc484d06fcecfb7d3e9394940 GIT binary patch literal 445 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 literal 0 HcmV?d00001 diff --git a/src/iceberg/test/resources/encryption/table/metadata/511c33d0-d9b9-4c80-bce0-b6fe4679785b-m0.avro b/src/iceberg/test/resources/encryption/table/metadata/511c33d0-d9b9-4c80-bce0-b6fe4679785b-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..8be2d3aab9b18f040f36eb167342e4219093ebfa GIT binary patch literal 7886 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 stM5|P5=o7*zvDl?Fcmzii#9^9QmaOj~}-T(jq literal 0 HcmV?d00001 diff --git a/src/iceberg/test/resources/encryption/table/metadata/95f19655-198b-4c2a-a7e5-e3cf75d5ab54-m0.avro b/src/iceberg/test/resources/encryption/table/metadata/95f19655-198b-4c2a-a7e5-e3cf75d5ab54-m0.avro new file mode 100644 index 0000000000000000000000000000000000000000..7e81d8faf6e76bd8553c21af3322269a50900f7a GIT binary patch literal 7844 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 literal 0 HcmV?d00001 diff --git a/src/iceberg/test/resources/encryption/table/metadata/snap-1122461367364127429-1-462964d7-9cf9-4547-a01b-29e4eef4bc25.avro b/src/iceberg/test/resources/encryption/table/metadata/snap-1122461367364127429-1-462964d7-9cf9-4547-a01b-29e4eef4bc25.avro new file mode 100644 index 0000000000000000000000000000000000000000..c45ab3ca34cc64d506e857a50b5673efcd9c4343 GIT binary patch literal 4940 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 Lll}Y=-4;LY1_{)I literal 0 HcmV?d00001 diff --git a/src/iceberg/test/resources/encryption/table/metadata/snap-5555854819025587960-1-d1977369-b04a-4cf3-a491-6090bc1572fc.avro b/src/iceberg/test/resources/encryption/table/metadata/snap-5555854819025587960-1-d1977369-b04a-4cf3-a491-6090bc1572fc.avro new file mode 100644 index 0000000000000000000000000000000000000000..eb6658a6f4acee9d1545225b70cdbaa84586815c GIT binary patch literal 5145 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 H^EC@BbmR}$ literal 0 HcmV?d00001 diff --git a/src/iceberg/test/resources/encryption/table/metadata/snap-564307663132197943-1-511c33d0-d9b9-4c80-bce0-b6fe4679785b.avro b/src/iceberg/test/resources/encryption/table/metadata/snap-564307663132197943-1-511c33d0-d9b9-4c80-bce0-b6fe4679785b.avro new file mode 100644 index 0000000000000000000000000000000000000000..bfc39890f1453c73979bec916b7a0a16cb6b358f GIT binary patch literal 4825 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 literal 0 HcmV?d00001 diff --git a/src/iceberg/test/resources/encryption/table/metadata/v1.metadata.json b/src/iceberg/test/resources/encryption/table/metadata/v1.metadata.json new file mode 100644 index 0000000000..274102950f --- /dev/null +++ b/src/iceberg/test/resources/encryption/table/metadata/v1.metadata.json @@ -0,0 +1 @@ +{"format-version":3,"table-uuid":"2091a278-2727-4bc0-8427-ffb04a2b7e6d","location":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table","last-sequence-number":0,"last-updated-ms":1791460592557,"last-column-id":2,"current-schema-id":0,"schemas":[{"type":"struct","schema-id":0,"fields":[{"id":1,"name":"id","required":true,"type":"long"},{"id":2,"name":"data","required":false,"type":"string"}]}],"default-spec-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"last-partition-id":999,"default-sort-order-id":0,"sort-orders":[{"order-id":0,"fields":[]}],"properties":{"encryption.key-id":"keyA","write.parquet.compression-codec":"zstd"},"current-snapshot-id":null,"next-row-id":0,"refs":{},"snapshots":[],"statistics":[],"partition-statistics":[],"snapshot-log":[],"metadata-log":[]} diff --git a/src/iceberg/test/resources/encryption/table/metadata/v2.metadata.json b/src/iceberg/test/resources/encryption/table/metadata/v2.metadata.json new file mode 100644 index 0000000000..0ffe050c5d --- /dev/null +++ b/src/iceberg/test/resources/encryption/table/metadata/v2.metadata.json @@ -0,0 +1 @@ +{"format-version":3,"table-uuid":"2091a278-2727-4bc0-8427-ffb04a2b7e6d","location":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table","last-sequence-number":1,"last-updated-ms":1791460593555,"last-column-id":2,"current-schema-id":0,"schemas":[{"type":"struct","schema-id":0,"fields":[{"id":1,"name":"id","required":true,"type":"long"},{"id":2,"name":"data","required":false,"type":"string"}]}],"default-spec-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"last-partition-id":999,"default-sort-order-id":0,"sort-orders":[{"order-id":0,"fields":[]}],"properties":{"encryption.key-id":"keyA","write.parquet.compression-codec":"zstd"},"current-snapshot-id":564307663132197943,"next-row-id":5,"encryption-keys":[{"key-id":"6kMyWaTMFqk5FVecTafB8w==","encrypted-key-metadata":"BA3fkOhBo//37HfxBA1lUEbSrgrv3k06ovUGyi70AINLvvCU9iCUdhwR9lM=","encrypted-by-id":"keyA","properties":{"KEY_TIMESTAMP":"1791460593549"}},{"key-id":"9Dpe+fdlQ+4lMH5jW0q1YQ==","encrypted-key-metadata":"UTlv697ZHJpwrMe7rfBpOBNsbrSKKZuHLJm2i/V0DiJmtTR3RxxBhYlxxBaDH6K9kpFRZ1YrkRCYLhgrY5pAyvj9KA==","encrypted-by-id":"6kMyWaTMFqk5FVecTafB8w=="}],"refs":{"main":{"snapshot-id":564307663132197943,"type":"branch"}},"snapshots":[{"sequence-number":1,"snapshot-id":564307663132197943,"timestamp-ms":1791460593552,"summary":{"operation":"append","manifests-created":"1","manifests-kept":"0","manifests-replaced":"0","added-data-files":"1","added-records":"5","added-files-size":"1791","changed-partition-count":"1","total-records":"5","total-files-size":"1791","total-data-files":"1","total-delete-files":"0","total-position-deletes":"0","total-equality-deletes":"0","iceberg-version":"Apache Iceberg 1.13.0-SNAPSHOT (commit 4d5811f2c701b15be4379686b17144503ed1e685)"},"manifest-list":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/snap-564307663132197943-1-511c33d0-d9b9-4c80-bce0-b6fe4679785b.avro","schema-id":0,"first-row-id":0,"added-rows":5,"key-id":"9Dpe+fdlQ+4lMH5jW0q1YQ=="}],"statistics":[],"partition-statistics":[],"snapshot-log":[{"timestamp-ms":1791460593552,"snapshot-id":564307663132197943}],"metadata-log":[{"timestamp-ms":1791460592557,"metadata-file":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/v1.metadata.json"}]} diff --git a/src/iceberg/test/resources/encryption/table/metadata/v3.metadata.json b/src/iceberg/test/resources/encryption/table/metadata/v3.metadata.json new file mode 100644 index 0000000000..e724cd20fc --- /dev/null +++ b/src/iceberg/test/resources/encryption/table/metadata/v3.metadata.json @@ -0,0 +1 @@ +{"format-version":3,"table-uuid":"2091a278-2727-4bc0-8427-ffb04a2b7e6d","location":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table","last-sequence-number":2,"last-updated-ms":1791460593713,"last-column-id":2,"current-schema-id":0,"schemas":[{"type":"struct","schema-id":0,"fields":[{"id":1,"name":"id","required":true,"type":"long"},{"id":2,"name":"data","required":false,"type":"string"}]}],"default-spec-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"last-partition-id":999,"default-sort-order-id":0,"sort-orders":[{"order-id":0,"fields":[]}],"properties":{"encryption.key-id":"keyA","write.parquet.compression-codec":"zstd"},"current-snapshot-id":1122461367364127429,"next-row-id":10,"encryption-keys":[{"key-id":"6kMyWaTMFqk5FVecTafB8w==","encrypted-key-metadata":"BA3fkOhBo//37HfxBA1lUEbSrgrv3k06ovUGyi70AINLvvCU9iCUdhwR9lM=","encrypted-by-id":"keyA","properties":{"KEY_TIMESTAMP":"1791460593549"}},{"key-id":"9Dpe+fdlQ+4lMH5jW0q1YQ==","encrypted-key-metadata":"UTlv697ZHJpwrMe7rfBpOBNsbrSKKZuHLJm2i/V0DiJmtTR3RxxBhYlxxBaDH6K9kpFRZ1YrkRCYLhgrY5pAyvj9KA==","encrypted-by-id":"6kMyWaTMFqk5FVecTafB8w=="},{"key-id":"LU90Mju0jbgvcsbifDCF0A==","encrypted-key-metadata":"2VLA6waTLBagemORlfjUdPRjmo3M3fPei7WkwX/q4S8aEM8CnAW8gllBDpEPXa/70jzMCvlVXkdg5x0CCmzoBKaSkA==","encrypted-by-id":"6kMyWaTMFqk5FVecTafB8w=="}],"refs":{"main":{"snapshot-id":1122461367364127429,"type":"branch"}},"snapshots":[{"sequence-number":1,"snapshot-id":564307663132197943,"timestamp-ms":1791460593552,"summary":{"operation":"append","manifests-created":"1","manifests-kept":"0","manifests-replaced":"0","added-data-files":"1","added-records":"5","added-files-size":"1791","changed-partition-count":"1","total-records":"5","total-files-size":"1791","total-data-files":"1","total-delete-files":"0","total-position-deletes":"0","total-equality-deletes":"0","iceberg-version":"Apache Iceberg 1.13.0-SNAPSHOT (commit 4d5811f2c701b15be4379686b17144503ed1e685)"},"manifest-list":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/snap-564307663132197943-1-511c33d0-d9b9-4c80-bce0-b6fe4679785b.avro","schema-id":0,"first-row-id":0,"added-rows":5,"key-id":"9Dpe+fdlQ+4lMH5jW0q1YQ=="},{"sequence-number":2,"snapshot-id":1122461367364127429,"parent-snapshot-id":564307663132197943,"timestamp-ms":1791460593713,"summary":{"operation":"append","manifests-created":"1","manifests-kept":"1","manifests-replaced":"0","added-data-files":"1","added-records":"5","added-files-size":"1793","changed-partition-count":"1","total-records":"10","total-files-size":"3584","total-data-files":"2","total-delete-files":"0","total-position-deletes":"0","total-equality-deletes":"0","iceberg-version":"Apache Iceberg 1.13.0-SNAPSHOT (commit 4d5811f2c701b15be4379686b17144503ed1e685)"},"manifest-list":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/snap-1122461367364127429-1-462964d7-9cf9-4547-a01b-29e4eef4bc25.avro","schema-id":0,"first-row-id":5,"added-rows":5,"key-id":"LU90Mju0jbgvcsbifDCF0A=="}],"statistics":[],"partition-statistics":[],"snapshot-log":[{"timestamp-ms":1791460593552,"snapshot-id":564307663132197943},{"timestamp-ms":1791460593713,"snapshot-id":1122461367364127429}],"metadata-log":[{"timestamp-ms":1791460592557,"metadata-file":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/v1.metadata.json"},{"timestamp-ms":1791460593555,"metadata-file":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/v2.metadata.json"}]} diff --git a/src/iceberg/test/resources/encryption/table/metadata/v4.metadata.json b/src/iceberg/test/resources/encryption/table/metadata/v4.metadata.json new file mode 100644 index 0000000000..2426069f2b --- /dev/null +++ b/src/iceberg/test/resources/encryption/table/metadata/v4.metadata.json @@ -0,0 +1 @@ +{"format-version":3,"table-uuid":"2091a278-2727-4bc0-8427-ffb04a2b7e6d","location":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table","last-sequence-number":3,"last-updated-ms":1791460593828,"last-column-id":2,"current-schema-id":0,"schemas":[{"type":"struct","schema-id":0,"fields":[{"id":1,"name":"id","required":true,"type":"long"},{"id":2,"name":"data","required":false,"type":"string"}]}],"default-spec-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"last-partition-id":999,"default-sort-order-id":0,"sort-orders":[{"order-id":0,"fields":[]}],"properties":{"encryption.key-id":"keyA","write.parquet.compression-codec":"zstd"},"current-snapshot-id":5112456484007752895,"next-row-id":13,"encryption-keys":[{"key-id":"6kMyWaTMFqk5FVecTafB8w==","encrypted-key-metadata":"BA3fkOhBo//37HfxBA1lUEbSrgrv3k06ovUGyi70AINLvvCU9iCUdhwR9lM=","encrypted-by-id":"keyA","properties":{"KEY_TIMESTAMP":"1791460593549"}},{"key-id":"9Dpe+fdlQ+4lMH5jW0q1YQ==","encrypted-key-metadata":"UTlv697ZHJpwrMe7rfBpOBNsbrSKKZuHLJm2i/V0DiJmtTR3RxxBhYlxxBaDH6K9kpFRZ1YrkRCYLhgrY5pAyvj9KA==","encrypted-by-id":"6kMyWaTMFqk5FVecTafB8w=="},{"key-id":"LU90Mju0jbgvcsbifDCF0A==","encrypted-key-metadata":"2VLA6waTLBagemORlfjUdPRjmo3M3fPei7WkwX/q4S8aEM8CnAW8gllBDpEPXa/70jzMCvlVXkdg5x0CCmzoBKaSkA==","encrypted-by-id":"6kMyWaTMFqk5FVecTafB8w=="},{"key-id":"ZkXVnuHz4Qlemc4wNaI8Eg==","encrypted-key-metadata":"qz9eUQScTKPymZ8tV+csNKXRhBF2hDHU7s0d7+AbbG10T6Zgqc/BVKXfL5FjYJ2KNQdCxpp4CAm3HlXpFbxR5HA+Kw==","encrypted-by-id":"6kMyWaTMFqk5FVecTafB8w=="}],"refs":{"main":{"snapshot-id":5112456484007752895,"type":"branch"}},"snapshots":[{"sequence-number":1,"snapshot-id":564307663132197943,"timestamp-ms":1791460593552,"summary":{"operation":"append","manifests-created":"1","manifests-kept":"0","manifests-replaced":"0","added-data-files":"1","added-records":"5","added-files-size":"1791","changed-partition-count":"1","total-records":"5","total-files-size":"1791","total-data-files":"1","total-delete-files":"0","total-position-deletes":"0","total-equality-deletes":"0","iceberg-version":"Apache Iceberg 1.13.0-SNAPSHOT (commit 4d5811f2c701b15be4379686b17144503ed1e685)"},"manifest-list":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/snap-564307663132197943-1-511c33d0-d9b9-4c80-bce0-b6fe4679785b.avro","schema-id":0,"first-row-id":0,"added-rows":5,"key-id":"9Dpe+fdlQ+4lMH5jW0q1YQ=="},{"sequence-number":2,"snapshot-id":1122461367364127429,"parent-snapshot-id":564307663132197943,"timestamp-ms":1791460593713,"summary":{"operation":"append","manifests-created":"1","manifests-kept":"1","manifests-replaced":"0","added-data-files":"1","added-records":"5","added-files-size":"1793","changed-partition-count":"1","total-records":"10","total-files-size":"3584","total-data-files":"2","total-delete-files":"0","total-position-deletes":"0","total-equality-deletes":"0","iceberg-version":"Apache Iceberg 1.13.0-SNAPSHOT (commit 4d5811f2c701b15be4379686b17144503ed1e685)"},"manifest-list":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/snap-1122461367364127429-1-462964d7-9cf9-4547-a01b-29e4eef4bc25.avro","schema-id":0,"first-row-id":5,"added-rows":5,"key-id":"LU90Mju0jbgvcsbifDCF0A=="},{"sequence-number":3,"snapshot-id":5112456484007752895,"parent-snapshot-id":1122461367364127429,"timestamp-ms":1791460593828,"summary":{"operation":"append","manifests-created":"1","manifests-kept":"2","manifests-replaced":"0","added-data-files":"1","added-records":"3","added-files-size":"445","changed-partition-count":"1","total-records":"13","total-files-size":"4029","total-data-files":"3","total-delete-files":"0","total-position-deletes":"0","total-equality-deletes":"0","iceberg-version":"Apache Iceberg 1.13.0-SNAPSHOT (commit 4d5811f2c701b15be4379686b17144503ed1e685)"},"manifest-list":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/snap-5112456484007752895-1-95f19655-198b-4c2a-a7e5-e3cf75d5ab54.avro","schema-id":0,"first-row-id":10,"added-rows":3,"key-id":"ZkXVnuHz4Qlemc4wNaI8Eg=="}],"statistics":[],"partition-statistics":[],"snapshot-log":[{"timestamp-ms":1791460593552,"snapshot-id":564307663132197943},{"timestamp-ms":1791460593713,"snapshot-id":1122461367364127429},{"timestamp-ms":1791460593828,"snapshot-id":5112456484007752895}],"metadata-log":[{"timestamp-ms":1791460592557,"metadata-file":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/v1.metadata.json"},{"timestamp-ms":1791460593555,"metadata-file":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/v2.metadata.json"},{"timestamp-ms":1791460593713,"metadata-file":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/v3.metadata.json"}]} diff --git a/src/iceberg/test/resources/encryption/table/metadata/v5.metadata.json b/src/iceberg/test/resources/encryption/table/metadata/v5.metadata.json new file mode 100644 index 0000000000..241ea609c3 --- /dev/null +++ b/src/iceberg/test/resources/encryption/table/metadata/v5.metadata.json @@ -0,0 +1 @@ +{"format-version":3,"table-uuid":"2091a278-2727-4bc0-8427-ffb04a2b7e6d","location":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table","last-sequence-number":4,"last-updated-ms":1791460593995,"last-column-id":2,"current-schema-id":0,"schemas":[{"type":"struct","schema-id":0,"fields":[{"id":1,"name":"id","required":true,"type":"long"},{"id":2,"name":"data","required":false,"type":"string"}]}],"default-spec-id":0,"partition-specs":[{"spec-id":0,"fields":[]}],"last-partition-id":999,"default-sort-order-id":0,"sort-orders":[{"order-id":0,"fields":[]}],"properties":{"encryption.key-id":"keyA","write.parquet.compression-codec":"zstd"},"current-snapshot-id":5555854819025587960,"next-row-id":13,"encryption-keys":[{"key-id":"6kMyWaTMFqk5FVecTafB8w==","encrypted-key-metadata":"BA3fkOhBo//37HfxBA1lUEbSrgrv3k06ovUGyi70AINLvvCU9iCUdhwR9lM=","encrypted-by-id":"keyA","properties":{"KEY_TIMESTAMP":"1791460593549"}},{"key-id":"9Dpe+fdlQ+4lMH5jW0q1YQ==","encrypted-key-metadata":"UTlv697ZHJpwrMe7rfBpOBNsbrSKKZuHLJm2i/V0DiJmtTR3RxxBhYlxxBaDH6K9kpFRZ1YrkRCYLhgrY5pAyvj9KA==","encrypted-by-id":"6kMyWaTMFqk5FVecTafB8w=="},{"key-id":"LU90Mju0jbgvcsbifDCF0A==","encrypted-key-metadata":"2VLA6waTLBagemORlfjUdPRjmo3M3fPei7WkwX/q4S8aEM8CnAW8gllBDpEPXa/70jzMCvlVXkdg5x0CCmzoBKaSkA==","encrypted-by-id":"6kMyWaTMFqk5FVecTafB8w=="},{"key-id":"ZkXVnuHz4Qlemc4wNaI8Eg==","encrypted-key-metadata":"qz9eUQScTKPymZ8tV+csNKXRhBF2hDHU7s0d7+AbbG10T6Zgqc/BVKXfL5FjYJ2KNQdCxpp4CAm3HlXpFbxR5HA+Kw==","encrypted-by-id":"6kMyWaTMFqk5FVecTafB8w=="},{"key-id":"VTWjHxdPbZFKJheX7XsR/Q==","encrypted-key-metadata":"EgZwxa5QZHjIWGAyrWOZiTfZEzOuQ1elzfuKke1DqxTQmGSHUTSNSxE0piZMRisSN/Ijp1EF7jbohWPi9OEr0BuBjQ==","encrypted-by-id":"6kMyWaTMFqk5FVecTafB8w=="}],"refs":{"main":{"snapshot-id":5555854819025587960,"type":"branch"}},"snapshots":[{"sequence-number":1,"snapshot-id":564307663132197943,"timestamp-ms":1791460593552,"summary":{"operation":"append","manifests-created":"1","manifests-kept":"0","manifests-replaced":"0","added-data-files":"1","added-records":"5","added-files-size":"1791","changed-partition-count":"1","total-records":"5","total-files-size":"1791","total-data-files":"1","total-delete-files":"0","total-position-deletes":"0","total-equality-deletes":"0","iceberg-version":"Apache Iceberg 1.13.0-SNAPSHOT (commit 4d5811f2c701b15be4379686b17144503ed1e685)"},"manifest-list":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/snap-564307663132197943-1-511c33d0-d9b9-4c80-bce0-b6fe4679785b.avro","schema-id":0,"first-row-id":0,"added-rows":5,"key-id":"9Dpe+fdlQ+4lMH5jW0q1YQ=="},{"sequence-number":2,"snapshot-id":1122461367364127429,"parent-snapshot-id":564307663132197943,"timestamp-ms":1791460593713,"summary":{"operation":"append","manifests-created":"1","manifests-kept":"1","manifests-replaced":"0","added-data-files":"1","added-records":"5","added-files-size":"1793","changed-partition-count":"1","total-records":"10","total-files-size":"3584","total-data-files":"2","total-delete-files":"0","total-position-deletes":"0","total-equality-deletes":"0","iceberg-version":"Apache Iceberg 1.13.0-SNAPSHOT (commit 4d5811f2c701b15be4379686b17144503ed1e685)"},"manifest-list":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/snap-1122461367364127429-1-462964d7-9cf9-4547-a01b-29e4eef4bc25.avro","schema-id":0,"first-row-id":5,"added-rows":5,"key-id":"LU90Mju0jbgvcsbifDCF0A=="},{"sequence-number":3,"snapshot-id":5112456484007752895,"parent-snapshot-id":1122461367364127429,"timestamp-ms":1791460593828,"summary":{"operation":"append","manifests-created":"1","manifests-kept":"2","manifests-replaced":"0","added-data-files":"1","added-records":"3","added-files-size":"445","changed-partition-count":"1","total-records":"13","total-files-size":"4029","total-data-files":"3","total-delete-files":"0","total-position-deletes":"0","total-equality-deletes":"0","iceberg-version":"Apache Iceberg 1.13.0-SNAPSHOT (commit 4d5811f2c701b15be4379686b17144503ed1e685)"},"manifest-list":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/snap-5112456484007752895-1-95f19655-198b-4c2a-a7e5-e3cf75d5ab54.avro","schema-id":0,"first-row-id":10,"added-rows":3,"key-id":"ZkXVnuHz4Qlemc4wNaI8Eg=="},{"sequence-number":4,"snapshot-id":5555854819025587960,"parent-snapshot-id":5112456484007752895,"timestamp-ms":1791460593995,"summary":{"operation":"delete","manifests-created":"1","manifests-kept":"3","manifests-replaced":"0","added-delete-files":"1","added-dvs":"1","added-files-size":"42","added-position-deletes":"1","changed-partition-count":"1","total-records":"13","total-files-size":"4071","total-data-files":"3","total-delete-files":"1","total-position-deletes":"1","total-equality-deletes":"0","iceberg-version":"Apache Iceberg 1.13.0-SNAPSHOT (commit 4d5811f2c701b15be4379686b17144503ed1e685)"},"manifest-list":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/snap-5555854819025587960-1-d1977369-b04a-4cf3-a491-6090bc1572fc.avro","schema-id":0,"first-row-id":13,"added-rows":0,"key-id":"VTWjHxdPbZFKJheX7XsR/Q=="}],"statistics":[],"partition-statistics":[],"snapshot-log":[{"timestamp-ms":1791460593552,"snapshot-id":564307663132197943},{"timestamp-ms":1791460593713,"snapshot-id":1122461367364127429},{"timestamp-ms":1791460593828,"snapshot-id":5112456484007752895},{"timestamp-ms":1791460593995,"snapshot-id":5555854819025587960}],"metadata-log":[{"timestamp-ms":1791460592557,"metadata-file":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/v1.metadata.json"},{"timestamp-ms":1791460593555,"metadata-file":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/v2.metadata.json"},{"timestamp-ms":1791460593713,"metadata-file":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/v3.metadata.json"},{"timestamp-ms":1791460593828,"metadata-file":"/tmp/iceberg-cpp-encryption-fixture/encrypted_table/metadata/v4.metadata.json"}]} diff --git a/src/iceberg/test/resources/encryption/table/metadata/version-hint.text b/src/iceberg/test/resources/encryption/table/metadata/version-hint.text new file mode 100644 index 0000000000..7ed6ff82de --- /dev/null +++ b/src/iceberg/test/resources/encryption/table/metadata/version-hint.text @@ -0,0 +1 @@ +5 diff --git a/src/iceberg/test/standard_encryption_manager_test.cc b/src/iceberg/test/standard_encryption_manager_test.cc new file mode 100644 index 0000000000..fb7f2469ed --- /dev/null +++ b/src/iceberg/test/standard_encryption_manager_test.cc @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "iceberg/encryption/standard_encryption_manager.h" + +#include + +#include + +#include "iceberg/encryption/encryption_register.h" +#include "iceberg/encryption/in_memory_kms.h" +#include "iceberg/snapshot.h" +#include "iceberg/table_metadata.h" +#include "iceberg/test/matchers.h" +#include "iceberg/test/test_resource.h" + +namespace iceberg { + +namespace { + +/// \brief Deterministic test bytes: byte i is (i * 31 + seed). +std::vector VectorBytes(size_t length, int seed) { + std::vector bytes(length); + for (size_t i = 0; i < length; ++i) { + bytes[i] = static_cast(static_cast(i) * 31 + seed); + } + return bytes; +} + +// The master key for "keyA" used by the Java fixture under +// test/resources/encryption/table. +const std::vector kMasterKeyA = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', '0', '1', '2', '3', '4', '5'}; + +/// Counts calls to the KMS. +class CountingKms : public InMemoryKms { + public: + using InMemoryKms::InMemoryKms; + Result> WrapKey(std::span key, + std::string_view id) override { + ++wraps; + return InMemoryKms::WrapKey(key, id); + } + Result> UnwrapKey(std::span wrapped, + std::string_view id) override { + ++unwraps; + return InMemoryKms::UnwrapKey(wrapped, id); + } + int wraps = 0; + int unwraps = 0; +}; + +std::shared_ptr MakeKms() { + return std::make_shared( + std::unordered_map>{{"keyA", kMasterKeyA}}); +} + +} // namespace + +TEST(StandardEncryptionManagerTest, DecryptsJavaManifestListKeys) { + ICEBERG_UNWRAP_OR_FAIL( + auto metadata, + ReadTableMetadataFromResource("encryption/table/metadata/v5.metadata.json")); + ASSERT_EQ(metadata->snapshots.size(), 4); + ASSERT_EQ(metadata->encryption_keys.size(), 5); + + // Through the registry, as catalogs do + encryption::RegisterAll(); + auto kms = MakeKms(); + ICEBERG_UNWRAP_OR_FAIL(auto manager, EncryptionManagerRegistry::Make( + {.table_key_id = "keyA", + .encryption_keys = metadata->encryption_keys, + .kms = kms})); + + for (const auto& snapshot : metadata->snapshots) { + ASSERT_TRUE(snapshot->key_id.has_value()); + ICEBERG_UNWRAP_OR_FAIL(auto decrypted, + manager->DecryptManifestListKeyMetadata(*snapshot->key_id)); + ICEBERG_UNWRAP_OR_FAIL(auto key_metadata, StandardKeyMetadata::Parse(decrypted)); + EXPECT_EQ(key_metadata.encryption_key.size(), 16); + EXPECT_EQ(key_metadata.aad_prefix->size(), 16); + // The trusted length is the stored (encrypted) size of the manifest list file + auto name = std::filesystem::path(snapshot->manifest_list).filename().string(); + EXPECT_EQ( + key_metadata.file_length, + std::filesystem::file_size(GetResourcePath("encryption/table/metadata/" + name))); + } + // One key encryption key for all snapshots, unwrapped once + EXPECT_EQ(kms->unwraps, 1); +} + +TEST(StandardEncryptionManagerTest, RegisterAndDecryptManifestListKeyMetadata) { + auto kms = MakeKms(); + ICEBERG_UNWRAP_OR_FAIL(auto manager, StandardEncryptionManager::Make( + {.table_key_id = "keyA", .kms = kms})); + + ICEBERG_UNWRAP_OR_FAIL(auto key_metadata, manager->NewKeyMetadata()); + EXPECT_EQ(key_metadata.encryption_key.size(), 16); + EXPECT_EQ(key_metadata.aad_prefix->size(), 16); + EXPECT_FALSE(key_metadata.file_length.has_value()); + // The manifest list length is required + EXPECT_THAT(manager->RegisterManifestListKeyMetadata(key_metadata), + IsError(ErrorKind::kInvalidArgument)); + key_metadata.file_length = 999; + + ICEBERG_UNWRAP_OR_FAIL(auto keys, + manager->RegisterManifestListKeyMetadata(key_metadata)); + EXPECT_EQ(keys.key_encryption_key.encrypted_by_id, "keyA"); + EXPECT_TRUE(keys.key_encryption_key.properties.contains("KEY_TIMESTAMP")); + EXPECT_EQ(keys.file_key.encrypted_by_id, keys.key_encryption_key.key_id); + EXPECT_EQ(kms->wraps, 1); + EXPECT_EQ(manager->encryption_keys().size(), 2); + + // A reader that only has the committed keys and the KMS can decrypt it + ICEBERG_UNWRAP_OR_FAIL(auto reader, + StandardEncryptionManager::Make( + {.table_key_id = "keyA", + .encryption_keys = {keys.key_encryption_key, keys.file_key}, + .kms = kms})); + ICEBERG_UNWRAP_OR_FAIL(auto decrypted, + reader->DecryptManifestListKeyMetadata(keys.file_key.key_id)); + EXPECT_EQ(decrypted, key_metadata.Serialize()); + + // Key encryption keys and unknown IDs are rejected + EXPECT_THAT(reader->DecryptManifestListKeyMetadata(keys.key_encryption_key.key_id), + IsError(ErrorKind::kInvalid)); + EXPECT_THAT(reader->DecryptManifestListKeyMetadata("missing"), + IsError(ErrorKind::kInvalid)); +} + +TEST(StandardEncryptionManagerTest, RotatesKeyEncryptionKeys) { + auto kms = MakeKms(); + ICEBERG_UNWRAP_OR_FAIL(auto manager, StandardEncryptionManager::Make( + {.table_key_id = "keyA", .kms = kms})); + StandardKeyMetadata key_metadata{.encryption_key = VectorBytes(16, 1), + .aad_prefix = VectorBytes(16, 2), + .file_length = 100}; + + ICEBERG_UNWRAP_OR_FAIL(auto first, + manager->RegisterManifestListKeyMetadata(key_metadata)); + ICEBERG_UNWRAP_OR_FAIL(auto second, + manager->RegisterManifestListKeyMetadata(key_metadata)); + EXPECT_EQ(second.key_encryption_key.key_id, first.key_encryption_key.key_id); + EXPECT_NE(second.file_key.key_id, first.file_key.key_id); + + manager->SetTestTimeShift(StandardEncryptionManager::kKekLifespan); + ICEBERG_UNWRAP_OR_FAIL(auto third, + manager->RegisterManifestListKeyMetadata(key_metadata)); + EXPECT_NE(third.key_encryption_key.key_id, first.key_encryption_key.key_id); + EXPECT_EQ(kms->wraps, 2); + + // Manifest lists encrypted with the old key encryption key stay readable + ICEBERG_UNWRAP_OR_FAIL(auto decrypted, + manager->DecryptManifestListKeyMetadata(first.file_key.key_id)); + EXPECT_EQ(decrypted, key_metadata.Serialize()); +} + +TEST(StandardEncryptionManagerTest, UnwrappedKeyCacheExpires) { + ICEBERG_UNWRAP_OR_FAIL( + auto metadata, + ReadTableMetadataFromResource("encryption/table/metadata/v5.metadata.json")); + auto kms = MakeKms(); + ICEBERG_UNWRAP_OR_FAIL(auto manager, StandardEncryptionManager::Make( + {.table_key_id = "keyA", + .encryption_keys = metadata->encryption_keys, + .kms = kms})); + const auto& key_id = *metadata->snapshots.front()->key_id; + ASSERT_THAT(manager->DecryptManifestListKeyMetadata(key_id), IsOk()); + ASSERT_THAT(manager->DecryptManifestListKeyMetadata(key_id), IsOk()); + EXPECT_EQ(kms->unwraps, 1); + + manager->SetTestTimeShift(StandardEncryptionManager::kUnwrappedKekCacheExpiry); + ASSERT_THAT(manager->DecryptManifestListKeyMetadata(key_id), IsOk()); + EXPECT_EQ(kms->unwraps, 2); +} + +TEST(StandardEncryptionManagerTest, DecryptingFileRequiresLength) { + auto kms = MakeKms(); + ICEBERG_UNWRAP_OR_FAIL(auto manager, StandardEncryptionManager::Make( + {.table_key_id = "keyA", .kms = kms})); + StandardKeyMetadata no_length{.encryption_key = VectorBytes(16, 1)}; + + class NullInputFile : public InputFile { + public: + std::string_view location() const override { return "file"; } + Result Size() const override { return 0; } + Result> Open() override { + return NotImplemented(""); + } + }; + EXPECT_THAT(manager->NewDecryptingInputFile(std::make_unique(), + no_length.Serialize()), + IsError(ErrorKind::kInvalid)); +} + +TEST(EncryptionManagerRegistryTest, ValidatesOptions) { + encryption::RegisterAll(); + EXPECT_TRUE(encryption::IsEncryptionAvailable()); + EXPECT_THAT(EncryptionManagerRegistry::Make({.table_key_id = "keyA"}), + IsError(ErrorKind::kInvalidArgument)); + EXPECT_THAT(EncryptionManagerRegistry::Make({.table_key_id = "", .kms = MakeKms()}), + IsError(ErrorKind::kInvalidArgument)); + EXPECT_THAT(EncryptionManagerRegistry::Make( + {.table_key_id = "keyA", .data_key_length = 20, .kms = MakeKms()}), + IsError(ErrorKind::kInvalidArgument)); +} + +} // namespace iceberg From 11c2537d3d7942867eca96ec726d46da709f9b05 Mon Sep 17 00:00:00 2001 From: shangxinli Date: Sat, 31 Oct 2026 18:02:13 -0700 Subject: [PATCH 2/3] fix: use designated initializers for CachedKek Resolves modernize-use-designated-initializers clang-tidy warnings. --- src/iceberg/encryption/standard_encryption_manager.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/iceberg/encryption/standard_encryption_manager.cc b/src/iceberg/encryption/standard_encryption_manager.cc index 40e3fb673a..1baaa7c4bc 100644 --- a/src/iceberg/encryption/standard_encryption_manager.cc +++ b/src/iceberg/encryption/standard_encryption_manager.cc @@ -153,7 +153,7 @@ Result> StandardEncryptionManager::UnwrapKek( ICEBERG_ASSIGN_OR_RAISE( auto kek, kms_->UnwrapKey(AsBytes(it->second.encrypted_key_metadata), table_key_id_)); - unwrapped_keks_.insert_or_assign(kek_id, CachedKek{kek, now}); + unwrapped_keks_.insert_or_assign(kek_id, CachedKek{.key = kek, .unwrapped_at_ms = now}); return kek; } @@ -206,7 +206,8 @@ Result StandardEncryptionManager::CurrentKekId() { .encrypted_key_metadata = ToString(wrapped), .encrypted_by_id = table_key_id_, .properties = {{std::string(kKeyTimestamp), std::to_string(now)}}}); - unwrapped_keks_.insert_or_assign(kek_id, CachedKek{std::move(kek), now}); + unwrapped_keks_.insert_or_assign( + kek_id, CachedKek{.key = std::move(kek), .unwrapped_at_ms = now}); return kek_id; } From eff1fbe12e2e1cafa647954385f0fdf975decd67 Mon Sep 17 00:00:00 2001 From: shangxinli Date: Wed, 4 Nov 2026 08:27:31 -0800 Subject: [PATCH 3/3] fix(encryption): install missing public headers and guard timestamp overflow Register encryption_manager.h, encryption_register.h, and standard_encryption_manager.h in the public header install lists so installed consumers can include the new encryption APIs. encryption_register.h is grouped as a bundle header (not encryption-only) so it stays available when the bundle is built with encryption disabled. Also reject negative KEY_TIMESTAMP values in ParseTimestamp to avoid signed overflow in CurrentKekId's now - timestamp comparison when the stored timestamp is corrupted to INT64_MIN. --- src/iceberg/cpp20_compatibility_internal.h | 3 ++ src/iceberg/encryption/CMakeLists.txt | 17 ++++++++--- .../encryption/standard_encryption_manager.cc | 2 +- .../test/standard_encryption_manager_test.cc | 30 +++++++++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/iceberg/cpp20_compatibility_internal.h b/src/iceberg/cpp20_compatibility_internal.h index d186e5bb0d..ad9edfb1b1 100644 --- a/src/iceberg/cpp20_compatibility_internal.h +++ b/src/iceberg/cpp20_compatibility_internal.h @@ -49,6 +49,7 @@ #include "iceberg/deletes/position_delete_range_consumer.h" #include "iceberg/deletes/roaring_position_bitmap.h" #include "iceberg/encryption/encrypted_key.h" +#include "iceberg/encryption/encryption_manager.h" #include "iceberg/encryption/key_management_client.h" #include "iceberg/encryption/standard_key_metadata.h" #include "iceberg/exception.h" @@ -213,6 +214,7 @@ # include "iceberg/avro/avro_reader.h" # include "iceberg/avro/avro_register.h" # include "iceberg/avro/avro_writer.h" +# include "iceberg/encryption/encryption_register.h" # include "iceberg/parquet/parquet_reader.h" # include "iceberg/parquet/parquet_register.h" # include "iceberg/parquet/parquet_writer.h" @@ -221,6 +223,7 @@ #ifdef ICEBERG_PUBLIC_HEADERS_ENCRYPTION # include "iceberg/encryption/aes_gcm_stream.h" # include "iceberg/encryption/in_memory_kms.h" +# include "iceberg/encryption/standard_encryption_manager.h" #endif // ICEBERG_PUBLIC_HEADERS_ENCRYPTION #ifdef ICEBERG_PUBLIC_HEADERS_HIVE diff --git a/src/iceberg/encryption/CMakeLists.txt b/src/iceberg/encryption/CMakeLists.txt index c490aa79ca..932b1deb75 100644 --- a/src/iceberg/encryption/CMakeLists.txt +++ b/src/iceberg/encryption/CMakeLists.txt @@ -18,13 +18,22 @@ iceberg_install_all_headers(iceberg/encryption PATTERN encrypted_key.h + encryption_manager.h key_management_client.h standard_key_metadata.h) -if(ICEBERG_BUILD_BUNDLE AND ICEBERG_ENCRYPTION) +if(ICEBERG_BUILD_BUNDLE) iceberg_install_all_headers(iceberg/encryption PATTERN - aes_gcm_stream.h - in_memory_kms.h + encryption_register.h HEADER_GROUP - ICEBERG_PUBLIC_HEADERS_ENCRYPTION) + ICEBERG_PUBLIC_HEADERS_BUNDLE) + if(ICEBERG_ENCRYPTION) + iceberg_install_all_headers(iceberg/encryption + PATTERN + aes_gcm_stream.h + in_memory_kms.h + standard_encryption_manager.h + HEADER_GROUP + ICEBERG_PUBLIC_HEADERS_ENCRYPTION) + endif() endif() diff --git a/src/iceberg/encryption/standard_encryption_manager.cc b/src/iceberg/encryption/standard_encryption_manager.cc index 1baaa7c4bc..fc4356758f 100644 --- a/src/iceberg/encryption/standard_encryption_manager.cc +++ b/src/iceberg/encryption/standard_encryption_manager.cc @@ -57,7 +57,7 @@ std::optional ParseTimestamp(const EncryptedKey& key) { int64_t value = 0; const auto& str = it->second; auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), value); - if (ec != std::errc{} || ptr != str.data() + str.size()) { + if (ec != std::errc{} || ptr != str.data() + str.size() || value < 0) { return std::nullopt; } return value; diff --git a/src/iceberg/test/standard_encryption_manager_test.cc b/src/iceberg/test/standard_encryption_manager_test.cc index fb7f2469ed..9bc7fe9479 100644 --- a/src/iceberg/test/standard_encryption_manager_test.cc +++ b/src/iceberg/test/standard_encryption_manager_test.cc @@ -171,6 +171,36 @@ TEST(StandardEncryptionManagerTest, RotatesKeyEncryptionKeys) { EXPECT_EQ(decrypted, key_metadata.Serialize()); } +TEST(StandardEncryptionManagerTest, IgnoresOutOfRangeKeyTimestamp) { + auto kms = MakeKms(); + ICEBERG_UNWRAP_OR_FAIL(auto manager, StandardEncryptionManager::Make( + {.table_key_id = "keyA", .kms = kms})); + StandardKeyMetadata key_metadata{.encryption_key = VectorBytes(16, 1), + .aad_prefix = VectorBytes(16, 2), + .file_length = 100}; + ICEBERG_UNWRAP_OR_FAIL(auto first, + manager->RegisterManifestListKeyMetadata(key_metadata)); + EXPECT_EQ(kms->wraps, 1); + + // A KEK whose KEY_TIMESTAMP is corrupted to INT64_MIN must never reach the + // "now - timestamp" subtraction, which would overflow. It should instead be + // treated as unparseable, forcing a fresh key encryption key to be created. + std::vector keys = manager->encryption_keys(); + for (auto& key : keys) { + if (key.encrypted_by_id == "keyA") { + key.properties[std::string(StandardEncryptionManager::kKeyTimestamp)] = + "-9223372036854775808"; + } + } + ICEBERG_UNWRAP_OR_FAIL( + auto reader, StandardEncryptionManager::Make( + {.table_key_id = "keyA", .encryption_keys = keys, .kms = kms})); + ICEBERG_UNWRAP_OR_FAIL(auto second, + reader->RegisterManifestListKeyMetadata(key_metadata)); + EXPECT_NE(second.key_encryption_key.key_id, first.key_encryption_key.key_id); + EXPECT_EQ(kms->wraps, 2); +} + TEST(StandardEncryptionManagerTest, UnwrappedKeyCacheExpires) { ICEBERG_UNWRAP_OR_FAIL( auto metadata,