You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@iceberg.apache.org by "shangxinli (via GitHub)" <gi...@apache.org> on 2026/10/16 18:50:23 UTC
[PR] feat: add AES GCM cipher and AES GCM Stream (AGS1) files [iceberg-cpp]
shangxinli opened a new pull request, #1030:
URL: https://github.com/apache/iceberg-cpp/pull/1030
## Summary
Step 4 of #988. Adds the crypto primitives of table encryption to the bundle library, built when `ICEBERG_ENCRYPTION` is ON. No behavior change for existing builds: everything here is new, and the default (`ICEBERG_ENCRYPTION=OFF`) compiles none of it.
- `AesGcmCipher` (OpenSSL EVP): AES-GCM with a random 12-byte nonce, in the `nonce | ciphertext | tag` layout of Java's `Ciphers`. Key lengths 16/24/32, AAD optional, every OpenSSL return code checked including `EVP_DecryptFinal_ex`, so a failed tag check is always an error rather than unauthenticated plaintext.
- `AesGcmInputFile` / `AesGcmOutputFile`: AES GCM Stream files per [`format/gcm-stream-spec.md`](https://github.com/apache/iceberg/blob/main/format/gcm-stream-spec.md), the format used for manifests, manifest lists, Avro data files and Puffin files. 1 MiB plaintext blocks, header `AGS1` + little-endian block size, block AAD = file AAD prefix followed by the block index as a little-endian int32, and only the last block may be partial. The reader is seekable and takes the encrypted length from the key metadata rather than the file, so truncation is detected; the writer reports the plaintext `Position()` and the encrypted `StoredLength()`.
`aes_gcm_stream.h` is the first public header that requires `ICEBERG_ENCRYPTION` while sharing a directory with unconditional ones (`encrypted_key.h`, `standard_key_metadata.h`). `ci/scripts/generate_public_headers.py` only classifies whole directories, so it grows a small per-header override table, and `cpp20_compatibility_test` gains a matching `ICEBERG_PUBLIC_HEADERS_ENCRYPTION` define.
## Java parity
Ported against `core/src/main/java/org/apache/iceberg/encryption/{Ciphers,AesGcmInputStream,AesGcmOutputStream,AesGcmInputFile,AesGcmOutputFile}.java`. Two places where the Java semantics are not obvious from the C++ base classes and are deliberately matched:
- `AesGcmOutputStream::Position()` keeps reporting the plaintext length after `Close()`. Closing flushes the final block, which resets the in-block position, so the value is captured first — Java does the same with its `finalPosition` field.
- An empty stream is written as one empty block, while a stream whose length is an exact multiple of the block size gets no trailing empty block.
`StandardKeyMetadata` records the stored length for every encrypted file because Java needs it for AGS1 reads; that part landed in #991.
## Test plan
- [x] `crypto_test` passes. The tests decrypt AGS1 streams of 0, 1 and 100 bytes and a KEK-encrypted manifest-list key metadata record, all produced by Iceberg Java, so a wire-format divergence fails rather than round-tripping against itself. Following #991, the fixtures are inlined byte arrays rather than checked-in binaries.
- [x] Round trips across block boundaries (0, 1, `block-1`, `block`, `block+1`, `2*block+17`) with write chunks that straddle blocks, plus random access across a boundary.
- [x] Wrong key, wrong AAD prefix, tampered ciphertext, truncated ciphertext, bad magic and a too-short stream all return errors.
- [x] `cpp20_compatibility_test` passes with `ICEBERG_ENCRYPTION=ON`.
- [x] `pre-commit` clean, including the generated public-header check.
Verified that the `Position()`-after-`Close()` assertion fails without its fix (reports `1048576` for a 100-byte stream).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] feat: add AES GCM cipher and AES GCM Stream (AGS1) files [iceberg-cpp]
Posted by "shangxinli (via GitHub)" <gi...@apache.org>.
shangxinli commented on code in PR #1030:
URL: https://github.com/apache/iceberg-cpp/pull/1030#discussion_r4303959571
##########
src/iceberg/encryption/aes_gcm_stream.cc:
##########
@@ -0,0 +1,344 @@
+/*
+ * 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/aes_gcm_stream.h"
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <limits>
+#include <utility>
+
+#include "iceberg/encryption/aes_gcm_internal.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+using encryption::AesGcmCipher;
+
+constexpr int64_t kPlainBlock = AesGcmStream::kPlainBlockSize;
+constexpr int64_t kCipherBlock = AesGcmStream::kCipherBlockSize;
+constexpr int64_t kBlockOverhead =
+ AesGcmStream::kNonceLength + AesGcmStream::kGcmTagLength;
+
+/// \brief Block AAD: file AAD prefix followed by the block index as a 4-byte LE int.
+std::vector<uint8_t> BlockAad(const std::vector<uint8_t>& aad_prefix,
+ int32_t block_index) {
+ std::vector<uint8_t> aad(aad_prefix);
+ for (int i = 0; i < 4; ++i) {
+ aad.push_back(
+ static_cast<uint8_t>((static_cast<uint32_t>(block_index) >> (8 * i)) & 0xFF));
+ }
+ return aad;
+}
+
+std::array<std::byte, AesGcmStream::kHeaderLength> HeaderBytes() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ std::memcpy(header.data(), AesGcmStream::kMagic.data(), AesGcmStream::kMagic.size());
+ for (int i = 0; i < 4; ++i) {
+ header[4 + i] = static_cast<std::byte>(
+ (static_cast<uint32_t>(AesGcmStream::kPlainBlockSize) >> (8 * i)) & 0xFF);
+ }
+ return header;
+}
+
+class AesGcmInputStream : public SeekableInputStream {
+ public:
+ AesGcmInputStream(std::unique_ptr<SeekableInputStream> source,
+ std::unique_ptr<AesGcmCipher> cipher, std::vector<uint8_t> aad_prefix,
+ int64_t encrypted_length)
+ : source_(std::move(source)),
+ cipher_(std::move(cipher)),
+ aad_prefix_(std::move(aad_prefix)),
+ cipher_block_(kCipherBlock),
+ plain_block_(kPlainBlock) {
+ int64_t stream_length = encrypted_length - AesGcmStream::kHeaderLength;
Review Comment:
Done - PlaintextLength now returns a Result and rejects the length before the stream is constructed, so Size() errors too; the int64 product itself can't actually overflow since num_full_blocks times the plain block size is always smaller than the stream length, but the Math.toIntExact bound on the block count was genuinely missing and is now enforced up front.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] feat: add AES GCM cipher and AES GCM Stream (AGS1) files [iceberg-cpp]
Posted by "wgtmac (via GitHub)" <gi...@apache.org>.
wgtmac commented on code in PR #1030:
URL: https://github.com/apache/iceberg-cpp/pull/1030#discussion_r4303397586
##########
src/iceberg/encryption/aes_gcm_stream.cc:
##########
@@ -0,0 +1,344 @@
+/*
+ * 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/aes_gcm_stream.h"
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <limits>
+#include <utility>
+
+#include "iceberg/encryption/aes_gcm_internal.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+using encryption::AesGcmCipher;
+
+constexpr int64_t kPlainBlock = AesGcmStream::kPlainBlockSize;
+constexpr int64_t kCipherBlock = AesGcmStream::kCipherBlockSize;
+constexpr int64_t kBlockOverhead =
+ AesGcmStream::kNonceLength + AesGcmStream::kGcmTagLength;
+
+/// \brief Block AAD: file AAD prefix followed by the block index as a 4-byte LE int.
+std::vector<uint8_t> BlockAad(const std::vector<uint8_t>& aad_prefix,
+ int32_t block_index) {
+ std::vector<uint8_t> aad(aad_prefix);
+ for (int i = 0; i < 4; ++i) {
+ aad.push_back(
+ static_cast<uint8_t>((static_cast<uint32_t>(block_index) >> (8 * i)) & 0xFF));
+ }
+ return aad;
+}
+
+std::array<std::byte, AesGcmStream::kHeaderLength> HeaderBytes() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ std::memcpy(header.data(), AesGcmStream::kMagic.data(), AesGcmStream::kMagic.size());
+ for (int i = 0; i < 4; ++i) {
+ header[4 + i] = static_cast<std::byte>(
+ (static_cast<uint32_t>(AesGcmStream::kPlainBlockSize) >> (8 * i)) & 0xFF);
+ }
+ return header;
+}
+
+class AesGcmInputStream : public SeekableInputStream {
+ public:
+ AesGcmInputStream(std::unique_ptr<SeekableInputStream> source,
+ std::unique_ptr<AesGcmCipher> cipher, std::vector<uint8_t> aad_prefix,
+ int64_t encrypted_length)
+ : source_(std::move(source)),
+ cipher_(std::move(cipher)),
+ aad_prefix_(std::move(aad_prefix)),
+ cipher_block_(kCipherBlock),
+ plain_block_(kPlainBlock) {
+ int64_t stream_length = encrypted_length - AesGcmStream::kHeaderLength;
Review Comment:
Please validate the encrypted length with checked arithmetic before deriving `plain_size`. A very large metadata length can overflow `num_full_blocks * kPlainBlockSize`; the current block-index check happens only when a block is decrypted, after `Size()` may already have returned an invalid length. Java bounds the block count with `Math.toIntExact`, so this path should reject the value before constructing the stream.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] feat: add AES GCM cipher and AES GCM Stream (AGS1) files [iceberg-cpp]
Posted by "wgtmac (via GitHub)" <gi...@apache.org>.
wgtmac commented on code in PR #1030:
URL: https://github.com/apache/iceberg-cpp/pull/1030#discussion_r4314054424
##########
src/iceberg/encryption/aes_gcm_stream.cc:
##########
@@ -0,0 +1,351 @@
+/*
+ * 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/aes_gcm_stream.h"
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <limits>
+#include <utility>
+
+#include "iceberg/encryption/aes_gcm_internal.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+using encryption::AesGcmCipher;
+
+constexpr int64_t kPlainBlock = AesGcmStream::kPlainBlockSize;
+constexpr int64_t kCipherBlock = AesGcmStream::kCipherBlockSize;
+constexpr int64_t kBlockOverhead =
+ AesGcmStream::kNonceLength + AesGcmStream::kGcmTagLength;
+
+/// \brief Block AAD: file AAD prefix followed by the block index as a 4-byte LE int.
+std::vector<uint8_t> BlockAad(const std::vector<uint8_t>& aad_prefix,
+ int32_t block_index) {
+ std::vector<uint8_t> aad(aad_prefix);
+ for (int i = 0; i < 4; ++i) {
+ aad.push_back(
+ static_cast<uint8_t>((static_cast<uint32_t>(block_index) >> (8 * i)) & 0xFF));
+ }
+ return aad;
+}
+
+std::array<std::byte, AesGcmStream::kHeaderLength> HeaderBytes() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ std::memcpy(header.data(), AesGcmStream::kMagic.data(), AesGcmStream::kMagic.size());
+ for (int i = 0; i < 4; ++i) {
+ header[4 + i] = static_cast<std::byte>(
+ (static_cast<uint32_t>(AesGcmStream::kPlainBlockSize) >> (8 * i)) & 0xFF);
+ }
+ return header;
+}
+
+class AesGcmInputStream : public SeekableInputStream {
+ public:
+ /// \brief Construct over an encrypted length already accepted by
+ /// AesGcmStream::PlaintextLength, which bounds the block count.
+ AesGcmInputStream(std::unique_ptr<SeekableInputStream> source,
+ std::unique_ptr<AesGcmCipher> cipher, std::vector<uint8_t> aad_prefix,
+ int64_t encrypted_length, int64_t plain_size)
+ : source_(std::move(source)),
+ cipher_(std::move(cipher)),
+ aad_prefix_(std::move(aad_prefix)),
+ cipher_block_(kCipherBlock),
+ plain_block_(kPlainBlock),
+ plain_size_(plain_size) {
+ int64_t stream_length = encrypted_length - AesGcmStream::kHeaderLength;
+ int64_t num_full_blocks = stream_length / kCipherBlock;
+ int64_t last_bytes = stream_length - num_full_blocks * kCipherBlock;
+ bool full_blocks_only = last_bytes == 0;
+ num_blocks_ = full_blocks_only ? num_full_blocks : num_full_blocks + 1;
+ last_cipher_block_size_ = full_blocks_only ? kCipherBlock : last_bytes;
+ }
+
+ Status ValidateHeader() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ ICEBERG_RETURN_UNEXPECTED(source_->ReadFully(0, header));
+ if (std::memcmp(header.data(), AesGcmStream::kMagic.data(),
+ AesGcmStream::kMagic.size()) != 0) {
+ return Invalid("Invalid GCM stream: magic does not match AGS1");
+ }
+ uint32_t plain_block_size = 0;
+ for (int i = 0; i < 4; ++i) {
+ plain_block_size |= static_cast<uint32_t>(header[4 + i]) << (8 * i);
+ }
+ if (plain_block_size != static_cast<uint32_t>(AesGcmStream::kPlainBlockSize)) {
+ return Invalid("Invalid GCM stream: block size {} != {}", plain_block_size,
+ AesGcmStream::kPlainBlockSize);
+ }
+ return {};
+ }
+
+ Result<int64_t> Position() const override { return position_; }
+
+ Status Seek(int64_t position) override {
+ if (position < 0 || position > plain_size_) {
+ return IOError("Invalid position {} for stream of length {}", position,
+ plain_size_);
+ }
+ position_ = position;
+ return {};
+ }
+
+ Result<int64_t> Read(std::span<std::byte> out) override {
+ int64_t total = 0;
+ while (std::cmp_less(total, out.size()) && position_ < plain_size_) {
+ int64_t block_index = position_ / kPlainBlock;
+ ICEBERG_RETURN_UNEXPECTED(DecryptBlock(block_index));
+ int64_t offset = position_ % kPlainBlock;
+ int64_t n = std::min<int64_t>(current_block_size_ - offset,
+ static_cast<int64_t>(out.size()) - total);
+ std::memcpy(out.data() + total, plain_block_.data() + offset, n);
+ total += n;
+ position_ += n;
+ }
+ return total;
+ }
+
+ Status ReadFully(int64_t position, std::span<std::byte> out) override {
+ if (position < 0 || position + static_cast<int64_t>(out.size()) > plain_size_) {
Review Comment:
This range check can still signed-overflow: ReadFully(INT64_MAX, a_one_byte_buffer) evaluates INT64_MAX + 1 before Seek rejects the position. Check position against plain_size_ first, then compare out.size() with plain_size_ - position, or use checked addition. Please add this small-buffer boundary case.
##########
src/iceberg/encryption/aes_gcm_stream.h:
##########
@@ -0,0 +1,92 @@
+/*
+ * 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/aes_gcm_stream.h
+/// AES GCM Stream (AGS1) files, see format/gcm-stream-spec.md.
+
+#include <array>
+#include <cstdint>
+#include <memory>
+#include <span>
+#include <vector>
+
+#include "iceberg/file_io.h"
+#include "iceberg/iceberg_bundle_export.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+/// \brief AES GCM Stream format constants.
+struct AesGcmStream {
+ static constexpr int32_t kPlainBlockSize = 1024 * 1024;
+ static constexpr int32_t kNonceLength = 12;
+ static constexpr int32_t kGcmTagLength = 16;
+ static constexpr int32_t kCipherBlockSize =
+ kPlainBlockSize + kNonceLength + kGcmTagLength;
+ static constexpr std::array<char, 4> kMagic = {'A', 'G', 'S', '1'};
+ static constexpr int32_t kHeaderLength = 8; // magic + plain block size (LE int32)
+ static constexpr int64_t kMinStreamLength =
+ kHeaderLength + kNonceLength + kGcmTagLength;
+
+ /// \brief Plaintext length of a stream with the given encrypted length.
+ ///
+ /// Returns an error if the length is shorter than an empty stream, implies more
+ /// than int32 max blocks, or ends in a block too short to hold a nonce and tag.
+ static Result<int64_t> PlaintextLength(int64_t encrypted_length);
Review Comment:
Please export this out-of-line public method with ICEBERG_BUNDLE_EXPORT. Shared bundle targets use hidden visibility and explicit DLL exports on Windows, so a downstream call to AesGcmStream::PlaintextLength otherwise fails to link. The current static-first tests do not cover this.
##########
src/iceberg/encryption/aes_gcm_stream.cc:
##########
@@ -0,0 +1,351 @@
+/*
+ * 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/aes_gcm_stream.h"
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <limits>
+#include <utility>
+
+#include "iceberg/encryption/aes_gcm_internal.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+using encryption::AesGcmCipher;
+
+constexpr int64_t kPlainBlock = AesGcmStream::kPlainBlockSize;
+constexpr int64_t kCipherBlock = AesGcmStream::kCipherBlockSize;
+constexpr int64_t kBlockOverhead =
+ AesGcmStream::kNonceLength + AesGcmStream::kGcmTagLength;
+
+/// \brief Block AAD: file AAD prefix followed by the block index as a 4-byte LE int.
+std::vector<uint8_t> BlockAad(const std::vector<uint8_t>& aad_prefix,
+ int32_t block_index) {
+ std::vector<uint8_t> aad(aad_prefix);
+ for (int i = 0; i < 4; ++i) {
+ aad.push_back(
+ static_cast<uint8_t>((static_cast<uint32_t>(block_index) >> (8 * i)) & 0xFF));
+ }
+ return aad;
+}
+
+std::array<std::byte, AesGcmStream::kHeaderLength> HeaderBytes() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ std::memcpy(header.data(), AesGcmStream::kMagic.data(), AesGcmStream::kMagic.size());
+ for (int i = 0; i < 4; ++i) {
+ header[4 + i] = static_cast<std::byte>(
+ (static_cast<uint32_t>(AesGcmStream::kPlainBlockSize) >> (8 * i)) & 0xFF);
+ }
+ return header;
+}
+
+class AesGcmInputStream : public SeekableInputStream {
+ public:
+ /// \brief Construct over an encrypted length already accepted by
+ /// AesGcmStream::PlaintextLength, which bounds the block count.
+ AesGcmInputStream(std::unique_ptr<SeekableInputStream> source,
+ std::unique_ptr<AesGcmCipher> cipher, std::vector<uint8_t> aad_prefix,
+ int64_t encrypted_length, int64_t plain_size)
+ : source_(std::move(source)),
+ cipher_(std::move(cipher)),
+ aad_prefix_(std::move(aad_prefix)),
+ cipher_block_(kCipherBlock),
+ plain_block_(kPlainBlock),
+ plain_size_(plain_size) {
+ int64_t stream_length = encrypted_length - AesGcmStream::kHeaderLength;
+ int64_t num_full_blocks = stream_length / kCipherBlock;
+ int64_t last_bytes = stream_length - num_full_blocks * kCipherBlock;
+ bool full_blocks_only = last_bytes == 0;
+ num_blocks_ = full_blocks_only ? num_full_blocks : num_full_blocks + 1;
+ last_cipher_block_size_ = full_blocks_only ? kCipherBlock : last_bytes;
+ }
+
+ Status ValidateHeader() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ ICEBERG_RETURN_UNEXPECTED(source_->ReadFully(0, header));
+ if (std::memcmp(header.data(), AesGcmStream::kMagic.data(),
+ AesGcmStream::kMagic.size()) != 0) {
+ return Invalid("Invalid GCM stream: magic does not match AGS1");
+ }
+ uint32_t plain_block_size = 0;
+ for (int i = 0; i < 4; ++i) {
+ plain_block_size |= static_cast<uint32_t>(header[4 + i]) << (8 * i);
+ }
+ if (plain_block_size != static_cast<uint32_t>(AesGcmStream::kPlainBlockSize)) {
+ return Invalid("Invalid GCM stream: block size {} != {}", plain_block_size,
+ AesGcmStream::kPlainBlockSize);
+ }
+ return {};
+ }
+
+ Result<int64_t> Position() const override { return position_; }
+
+ Status Seek(int64_t position) override {
+ if (position < 0 || position > plain_size_) {
+ return IOError("Invalid position {} for stream of length {}", position,
+ plain_size_);
+ }
+ position_ = position;
+ return {};
+ }
+
+ Result<int64_t> Read(std::span<std::byte> out) override {
+ int64_t total = 0;
+ while (std::cmp_less(total, out.size()) && position_ < plain_size_) {
Review Comment:
Please authenticate the sole block of an empty AGS1 stream before reporting EOF. With plain_size_ == 0, this loop never calls DecryptBlock, so a wrong key/AAD, a corrupted tag, or a file truncated to just its header is accepted as an empty file. Java's testEmptyFile explicitly checks this; please add the same negative cases here.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] feat: add AES GCM cipher and AES GCM Stream (AGS1) files [iceberg-cpp]
Posted by "shangxinli (via GitHub)" <gi...@apache.org>.
shangxinli commented on code in PR #1030:
URL: https://github.com/apache/iceberg-cpp/pull/1030#discussion_r4328290205
##########
src/iceberg/encryption/aes_gcm_stream.cc:
##########
@@ -0,0 +1,351 @@
+/*
+ * 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/aes_gcm_stream.h"
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <limits>
+#include <utility>
+
+#include "iceberg/encryption/aes_gcm_internal.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+using encryption::AesGcmCipher;
+
+constexpr int64_t kPlainBlock = AesGcmStream::kPlainBlockSize;
+constexpr int64_t kCipherBlock = AesGcmStream::kCipherBlockSize;
+constexpr int64_t kBlockOverhead =
+ AesGcmStream::kNonceLength + AesGcmStream::kGcmTagLength;
+
+/// \brief Block AAD: file AAD prefix followed by the block index as a 4-byte LE int.
+std::vector<uint8_t> BlockAad(const std::vector<uint8_t>& aad_prefix,
+ int32_t block_index) {
+ std::vector<uint8_t> aad(aad_prefix);
+ for (int i = 0; i < 4; ++i) {
+ aad.push_back(
+ static_cast<uint8_t>((static_cast<uint32_t>(block_index) >> (8 * i)) & 0xFF));
+ }
+ return aad;
+}
+
+std::array<std::byte, AesGcmStream::kHeaderLength> HeaderBytes() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ std::memcpy(header.data(), AesGcmStream::kMagic.data(), AesGcmStream::kMagic.size());
+ for (int i = 0; i < 4; ++i) {
+ header[4 + i] = static_cast<std::byte>(
+ (static_cast<uint32_t>(AesGcmStream::kPlainBlockSize) >> (8 * i)) & 0xFF);
+ }
+ return header;
+}
+
+class AesGcmInputStream : public SeekableInputStream {
+ public:
+ /// \brief Construct over an encrypted length already accepted by
+ /// AesGcmStream::PlaintextLength, which bounds the block count.
+ AesGcmInputStream(std::unique_ptr<SeekableInputStream> source,
+ std::unique_ptr<AesGcmCipher> cipher, std::vector<uint8_t> aad_prefix,
+ int64_t encrypted_length, int64_t plain_size)
+ : source_(std::move(source)),
+ cipher_(std::move(cipher)),
+ aad_prefix_(std::move(aad_prefix)),
+ cipher_block_(kCipherBlock),
+ plain_block_(kPlainBlock),
+ plain_size_(plain_size) {
+ int64_t stream_length = encrypted_length - AesGcmStream::kHeaderLength;
+ int64_t num_full_blocks = stream_length / kCipherBlock;
+ int64_t last_bytes = stream_length - num_full_blocks * kCipherBlock;
+ bool full_blocks_only = last_bytes == 0;
+ num_blocks_ = full_blocks_only ? num_full_blocks : num_full_blocks + 1;
+ last_cipher_block_size_ = full_blocks_only ? kCipherBlock : last_bytes;
+ }
+
+ Status ValidateHeader() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ ICEBERG_RETURN_UNEXPECTED(source_->ReadFully(0, header));
+ if (std::memcmp(header.data(), AesGcmStream::kMagic.data(),
+ AesGcmStream::kMagic.size()) != 0) {
+ return Invalid("Invalid GCM stream: magic does not match AGS1");
+ }
+ uint32_t plain_block_size = 0;
+ for (int i = 0; i < 4; ++i) {
+ plain_block_size |= static_cast<uint32_t>(header[4 + i]) << (8 * i);
+ }
+ if (plain_block_size != static_cast<uint32_t>(AesGcmStream::kPlainBlockSize)) {
+ return Invalid("Invalid GCM stream: block size {} != {}", plain_block_size,
+ AesGcmStream::kPlainBlockSize);
+ }
+ return {};
+ }
+
+ Result<int64_t> Position() const override { return position_; }
+
+ Status Seek(int64_t position) override {
+ if (position < 0 || position > plain_size_) {
+ return IOError("Invalid position {} for stream of length {}", position,
+ plain_size_);
+ }
+ position_ = position;
+ return {};
+ }
+
+ Result<int64_t> Read(std::span<std::byte> out) override {
+ int64_t total = 0;
+ while (std::cmp_less(total, out.size()) && position_ < plain_size_) {
Review Comment:
Done - the sole block is now decrypted when the stream is opened, with negative cases for a wrong key, a wrong AAD prefix, a corrupt tag and truncation behind the trusted length.
##########
src/iceberg/encryption/aes_gcm_stream.h:
##########
@@ -0,0 +1,92 @@
+/*
+ * 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/aes_gcm_stream.h
+/// AES GCM Stream (AGS1) files, see format/gcm-stream-spec.md.
+
+#include <array>
+#include <cstdint>
+#include <memory>
+#include <span>
+#include <vector>
+
+#include "iceberg/file_io.h"
+#include "iceberg/iceberg_bundle_export.h"
+#include "iceberg/result.h"
+
+namespace iceberg {
+
+/// \brief AES GCM Stream format constants.
+struct AesGcmStream {
+ static constexpr int32_t kPlainBlockSize = 1024 * 1024;
+ static constexpr int32_t kNonceLength = 12;
+ static constexpr int32_t kGcmTagLength = 16;
+ static constexpr int32_t kCipherBlockSize =
+ kPlainBlockSize + kNonceLength + kGcmTagLength;
+ static constexpr std::array<char, 4> kMagic = {'A', 'G', 'S', '1'};
+ static constexpr int32_t kHeaderLength = 8; // magic + plain block size (LE int32)
+ static constexpr int64_t kMinStreamLength =
+ kHeaderLength + kNonceLength + kGcmTagLength;
+
+ /// \brief Plaintext length of a stream with the given encrypted length.
+ ///
+ /// Returns an error if the length is shorter than an empty stream, implies more
+ /// than int32 max blocks, or ends in a block too short to hold a nonce and tag.
+ static Result<int64_t> PlaintextLength(int64_t encrypted_length);
Review Comment:
Added ICEBERG_BUNDLE_EXPORT on it.
##########
src/iceberg/encryption/aes_gcm_stream.cc:
##########
@@ -0,0 +1,351 @@
+/*
+ * 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/aes_gcm_stream.h"
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <limits>
+#include <utility>
+
+#include "iceberg/encryption/aes_gcm_internal.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+using encryption::AesGcmCipher;
+
+constexpr int64_t kPlainBlock = AesGcmStream::kPlainBlockSize;
+constexpr int64_t kCipherBlock = AesGcmStream::kCipherBlockSize;
+constexpr int64_t kBlockOverhead =
+ AesGcmStream::kNonceLength + AesGcmStream::kGcmTagLength;
+
+/// \brief Block AAD: file AAD prefix followed by the block index as a 4-byte LE int.
+std::vector<uint8_t> BlockAad(const std::vector<uint8_t>& aad_prefix,
+ int32_t block_index) {
+ std::vector<uint8_t> aad(aad_prefix);
+ for (int i = 0; i < 4; ++i) {
+ aad.push_back(
+ static_cast<uint8_t>((static_cast<uint32_t>(block_index) >> (8 * i)) & 0xFF));
+ }
+ return aad;
+}
+
+std::array<std::byte, AesGcmStream::kHeaderLength> HeaderBytes() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ std::memcpy(header.data(), AesGcmStream::kMagic.data(), AesGcmStream::kMagic.size());
+ for (int i = 0; i < 4; ++i) {
+ header[4 + i] = static_cast<std::byte>(
+ (static_cast<uint32_t>(AesGcmStream::kPlainBlockSize) >> (8 * i)) & 0xFF);
+ }
+ return header;
+}
+
+class AesGcmInputStream : public SeekableInputStream {
+ public:
+ /// \brief Construct over an encrypted length already accepted by
+ /// AesGcmStream::PlaintextLength, which bounds the block count.
+ AesGcmInputStream(std::unique_ptr<SeekableInputStream> source,
+ std::unique_ptr<AesGcmCipher> cipher, std::vector<uint8_t> aad_prefix,
+ int64_t encrypted_length, int64_t plain_size)
+ : source_(std::move(source)),
+ cipher_(std::move(cipher)),
+ aad_prefix_(std::move(aad_prefix)),
+ cipher_block_(kCipherBlock),
+ plain_block_(kPlainBlock),
+ plain_size_(plain_size) {
+ int64_t stream_length = encrypted_length - AesGcmStream::kHeaderLength;
+ int64_t num_full_blocks = stream_length / kCipherBlock;
+ int64_t last_bytes = stream_length - num_full_blocks * kCipherBlock;
+ bool full_blocks_only = last_bytes == 0;
+ num_blocks_ = full_blocks_only ? num_full_blocks : num_full_blocks + 1;
+ last_cipher_block_size_ = full_blocks_only ? kCipherBlock : last_bytes;
+ }
+
+ Status ValidateHeader() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ ICEBERG_RETURN_UNEXPECTED(source_->ReadFully(0, header));
+ if (std::memcmp(header.data(), AesGcmStream::kMagic.data(),
+ AesGcmStream::kMagic.size()) != 0) {
+ return Invalid("Invalid GCM stream: magic does not match AGS1");
+ }
+ uint32_t plain_block_size = 0;
+ for (int i = 0; i < 4; ++i) {
+ plain_block_size |= static_cast<uint32_t>(header[4 + i]) << (8 * i);
+ }
+ if (plain_block_size != static_cast<uint32_t>(AesGcmStream::kPlainBlockSize)) {
+ return Invalid("Invalid GCM stream: block size {} != {}", plain_block_size,
+ AesGcmStream::kPlainBlockSize);
+ }
+ return {};
+ }
+
+ Result<int64_t> Position() const override { return position_; }
+
+ Status Seek(int64_t position) override {
+ if (position < 0 || position > plain_size_) {
+ return IOError("Invalid position {} for stream of length {}", position,
+ plain_size_);
+ }
+ position_ = position;
+ return {};
+ }
+
+ Result<int64_t> Read(std::span<std::byte> out) override {
+ int64_t total = 0;
+ while (std::cmp_less(total, out.size()) && position_ < plain_size_) {
+ int64_t block_index = position_ / kPlainBlock;
+ ICEBERG_RETURN_UNEXPECTED(DecryptBlock(block_index));
+ int64_t offset = position_ % kPlainBlock;
+ int64_t n = std::min<int64_t>(current_block_size_ - offset,
+ static_cast<int64_t>(out.size()) - total);
+ std::memcpy(out.data() + total, plain_block_.data() + offset, n);
+ total += n;
+ position_ += n;
+ }
+ return total;
+ }
+
+ Status ReadFully(int64_t position, std::span<std::byte> out) override {
+ if (position < 0 || position + static_cast<int64_t>(out.size()) > plain_size_) {
Review Comment:
Reordered - the position is bounded first and the size compared against the remainder, with a one-byte read at int64 max covering it.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] feat: add AES GCM cipher and AES GCM Stream (AGS1) files [iceberg-cpp]
Posted by "shangxinli (via GitHub)" <gi...@apache.org>.
shangxinli commented on code in PR #1030:
URL: https://github.com/apache/iceberg-cpp/pull/1030#discussion_r4303959571
##########
src/iceberg/encryption/aes_gcm_stream.cc:
##########
@@ -0,0 +1,344 @@
+/*
+ * 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/aes_gcm_stream.h"
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <limits>
+#include <utility>
+
+#include "iceberg/encryption/aes_gcm_internal.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg {
+
+namespace {
+
+using encryption::AesGcmCipher;
+
+constexpr int64_t kPlainBlock = AesGcmStream::kPlainBlockSize;
+constexpr int64_t kCipherBlock = AesGcmStream::kCipherBlockSize;
+constexpr int64_t kBlockOverhead =
+ AesGcmStream::kNonceLength + AesGcmStream::kGcmTagLength;
+
+/// \brief Block AAD: file AAD prefix followed by the block index as a 4-byte LE int.
+std::vector<uint8_t> BlockAad(const std::vector<uint8_t>& aad_prefix,
+ int32_t block_index) {
+ std::vector<uint8_t> aad(aad_prefix);
+ for (int i = 0; i < 4; ++i) {
+ aad.push_back(
+ static_cast<uint8_t>((static_cast<uint32_t>(block_index) >> (8 * i)) & 0xFF));
+ }
+ return aad;
+}
+
+std::array<std::byte, AesGcmStream::kHeaderLength> HeaderBytes() {
+ std::array<std::byte, AesGcmStream::kHeaderLength> header{};
+ std::memcpy(header.data(), AesGcmStream::kMagic.data(), AesGcmStream::kMagic.size());
+ for (int i = 0; i < 4; ++i) {
+ header[4 + i] = static_cast<std::byte>(
+ (static_cast<uint32_t>(AesGcmStream::kPlainBlockSize) >> (8 * i)) & 0xFF);
+ }
+ return header;
+}
+
+class AesGcmInputStream : public SeekableInputStream {
+ public:
+ AesGcmInputStream(std::unique_ptr<SeekableInputStream> source,
+ std::unique_ptr<AesGcmCipher> cipher, std::vector<uint8_t> aad_prefix,
+ int64_t encrypted_length)
+ : source_(std::move(source)),
+ cipher_(std::move(cipher)),
+ aad_prefix_(std::move(aad_prefix)),
+ cipher_block_(kCipherBlock),
+ plain_block_(kPlainBlock) {
+ int64_t stream_length = encrypted_length - AesGcmStream::kHeaderLength;
Review Comment:
Fixed - the block count is now bounded before the stream is constructed, so Size() rejects it too.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org
Re: [PR] feat: add AES GCM cipher and AES GCM Stream (AGS1) files [iceberg-cpp]
Posted by "wgtmac (via GitHub)" <gi...@apache.org>.
wgtmac merged PR #1030:
URL: https://github.com/apache/iceberg-cpp/pull/1030
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscribe@iceberg.apache.org
For additional commands, e-mail: issues-help@iceberg.apache.org